Skip to content

Instantly share code, notes, and snippets.

@dezinezync
Last active December 5, 2023 07:36
Show Gist options
  • Save dezinezync/724d93dc8d95fe06a80881f39802c2aa to your computer and use it in GitHub Desktop.
Save dezinezync/724d93dc8d95fe06a80881f39802c2aa to your computer and use it in GitHub Desktop.
DZShadowsView
//
// DZShadowsView.swift
//
//
// Created by Nikhil Nigade on 16/12/21.
//
#if !os(watchOS)
#if os(iOS)
import UIKit
#elseif os(macOS)
import AppKit
#endif
import EasyPeasy
#if os(iOS)
public typealias RealColor = UIColor
#else
public typealias RealColor = NSColor
#endif
public final class DZShadowsView: RealView {
#if os(iOS)
public let contentView: RealView = UIView()
#else
public let contentView: RealView = FlippedView()
#endif
public struct Shadow: Hashable {
var radius: Double
var offset: CGSize
var color: RealColor
var opacity: Float
/// Create a shadow from params
/// - Parameters:
/// - radius: radius of the shadow. See `sketchShadow` for more info.
/// - offset: offset for the shadow
/// - color: color of the shadow
/// - opacity: opacity of the shadow (don't set on the color)
/// - sketchShadow: true if the shadow is being created from Sketch's layer panel. When true, the radius is halved.
public init(radius: Double, offset: CGSize, color: RealColor, opacity: Float, sketchShadow: Bool = false) {
self.radius = radius / (sketchShadow ? 2.0 : 1.0)
self.offset = offset
self.color = color
self.opacity = opacity
}
public func hash(into hasher: inout Hasher) {
hasher.combine(radius)
hasher.combine(offset.width)
hasher.combine(offset.height)
hasher.combine(opacity)
hasher.combine(color.description)
}
}
/// The shadows to setup on this view.
public var shadows: [Shadow] = [] {
didSet {
if oldValue != shadows {
setupShadows()
}
}
}
public override init(frame: CGRect) {
super.init(frame: frame)
commonInit()
}
public required init?(coder: NSCoder) {
super.init(coder: coder)
commonInit()
}
private func commonInit() {
#if os(iOS)
layer.masksToBounds = false
contentView.layer.masksToBounds = true
contentView.backgroundColor = .systemBackground
#elseif os(macOS)
contentView.wantsLayer = true
layer?.masksToBounds = false
contentView.layer?.masksToBounds = true
contentView.backgroundColor = .windowBackgroundColor
#endif
contentView.translatesAutoresizingMaskIntoConstraints = false
super.addSubview(contentView)
contentView.easy.layout(Edges())
}
public override func addSubview(_ aView: RealView) {
contentView.addSubview(aView)
}
public func setNeedsUpdateShadows() {
setupShadows()
#if os(iOS)
setNeedsLayout()
setNeedsDisplay()
#elseif os(macOS)
layoutSubtreeIfNeeded()
setNeedsDisplay(frame)
#endif
}
private var layers: [CALayer] = []
private func setupShadows() {
for layer in layers {
layer.removeFromSuperlayer()
}
for shadow in shadows {
let sublayer = CALayer()
sublayer.frame = bounds
#if os(iOS)
sublayer.cornerRadius = contentView.layer.cornerRadius
sublayer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: contentView.layer.cornerRadius).cgPath
#elseif os(macOS)
sublayer.cornerRadius = contentView.layer?.cornerRadius ?? 5
sublayer.shadowPath = NSBezierPath(roundedRect: bounds, xRadius: contentView.layer!.cornerRadius, yRadius: contentView.layer!.cornerRadius).cgPath
#endif
sublayer.shadowOpacity = shadow.opacity
sublayer.shadowColor = shadow.color.cgColor
sublayer.shadowOffset = shadow.offset
sublayer.shadowRadius = shadow.radius
#if os(iOS)
layer.addSublayer(sublayer)
#elseif os(macOS)
layer?.addSublayer(sublayer)
#endif
layers.append(sublayer)
}
}
#if os(iOS)
public override func layoutSubviews() {
backgroundColor = .clear
for sublayer in layer.sublayers ?? [] {
sublayer.frame = bounds
sublayer.shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: contentView.layer.cornerRadius).cgPath
}
bringSubviewToFront(contentView)
}
#elseif os(macOS)
public override func layout() {
super.layout()
backgroundColor = .clear
contentView.layer!.removeFromSuperlayer()
for sublayer in layer?.sublayers ?? [] {
sublayer.frame = bounds
sublayer.shadowPath = NSBezierPath(roundedRect: bounds, xRadius: contentView.layer!.cornerRadius, yRadius: contentView.layer!.cornerRadius).cgPath
}
// brings subview to front
layer?.addSublayer(contentView.layer!)
}
#endif
}
#if canImport(Cocoa)
public extension NSBezierPath {
var cgPath: CGPath {
let path = CGMutablePath()
var points = [CGPoint](repeating: .zero, count: 3)
for i in 0 ..< self.elementCount {
let type = self.element(at: i, associatedPoints: &points)
switch type {
case .moveTo:
path.move(to: points[0])
case .lineTo:
path.addLine(to: points[0])
case .curveTo:
path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath:
path.closeSubpath()
@unknown default:
break
}
}
return path
}
}
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment