├── .gitignore
├── .swiftpm
└── xcode
│ └── package.xcworkspace
│ └── contents.xcworkspacedata
├── Package.swift
├── README.md
├── Sources
└── Surface
│ ├── CALayer+Animate.swift
│ ├── CALayer+Shadow.swift
│ ├── MotionManager.swift
│ ├── Neumorphism.swift
│ ├── Presets.swift
│ ├── Shadow.swift
│ └── Surface.swift
├── Tests
├── LinuxMain.swift
└── SurfaceTests
│ ├── SurfaceTests.swift
│ └── XCTestManifests.swift
└── docs_
├── button.gif
└── switch.gif
/.gitignore:
--------------------------------------------------------------------------------
1 | # Xcode
2 | #
3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
4 |
5 | ## User settings
6 | xcuserdata/
7 |
8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9)
9 | *.xcscmblueprint
10 | *.xccheckout
11 |
12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4)
13 | build/
14 | DerivedData/
15 | *.moved-aside
16 | *.pbxuser
17 | !default.pbxuser
18 | *.mode1v3
19 | !default.mode1v3
20 | *.mode2v3
21 | !default.mode2v3
22 | *.perspectivev3
23 | !default.perspectivev3
24 |
25 | ## Obj-C/Swift specific
26 | *.hmap
27 |
28 | ## App packaging
29 | *.ipa
30 | *.dSYM.zip
31 | *.dSYM
32 |
33 | ## Playgrounds
34 | timeline.xctimeline
35 | playground.xcworkspace
36 |
37 | # Swift Package Manager
38 | #
39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
40 | # Packages/
41 | # Package.pins
42 | # Package.resolved
43 | # *.xcodeproj
44 | #
45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
46 | # hence it is not needed unless you have added a package configuration file to your project
47 | # .swiftpm
48 |
49 | .build/
50 |
51 | # CocoaPods
52 | #
53 | # We recommend against adding the Pods directory to your .gitignore. However
54 | # you should judge for yourself, the pros and cons are mentioned at:
55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
56 | #
57 | # Pods/
58 | #
59 | # Add this line if you want to avoid checking in source code from the Xcode workspace
60 | # *.xcworkspace
61 |
62 | # Carthage
63 | #
64 | # Add this line if you want to avoid checking in source code from Carthage dependencies.
65 | # Carthage/Checkouts
66 |
67 | Carthage/Build/
68 |
69 | # Accio dependency management
70 | Dependencies/
71 | .accio/
72 |
73 | # fastlane
74 | #
75 | # It is recommended to not store the screenshots in the git repo.
76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed.
77 | # For more information about the recommended setup visit:
78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control
79 |
80 | fastlane/report.xml
81 | fastlane/Preview.html
82 | fastlane/screenshots/**/*.png
83 | fastlane/test_output
84 |
85 | # Code Injection
86 | #
87 | # After new code Injection tools there's a generated folder /iOSInjectionProject
88 | # https://github.com/johnno1962/injectionforxcode
89 |
90 | iOSInjectionProject/
91 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version:5.1
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "Surface",
8 | platforms: [
9 | .iOS(.v13),
10 | .macOS(.v10_15),
11 | .tvOS(.v13),
12 | .watchOS(.v6)
13 | ],
14 | products: [
15 | // Products define the executables and libraries produced by a package, and make them visible to other packages.
16 | .library(
17 | name: "Surface",
18 | targets: ["Surface"]),
19 | ],
20 | dependencies: [
21 | // Dependencies declare other packages that this package depends on.
22 | // .package(url: /* package url */, from: "1.0.0"),
23 | ],
24 | targets: [
25 | // Targets are the basic building blocks of a package. A target can define a module or a test suite.
26 | // Targets can depend on other targets in this package, and on products in packages which this package depends on.
27 | .target(
28 | name: "Surface",
29 | dependencies: []),
30 | .testTarget(
31 | name: "SurfaceTests",
32 | dependencies: ["Surface"]),
33 | ]
34 | )
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Surface [](#)
2 |
3 | **Neumorphic** shadow example:
4 |
5 | ```swift
6 | let view = SurfaceView()
7 | view.frame = ...
8 | view.layer.cornerRadius = ...
9 | view.surfaceLayer.shadow = Shadow(preset: .convex1)
10 | view.surfaceLayer.useDeviceMotionToCastShadow = true
11 | addSubview(view)
12 | ```
13 |
14 | The cast shadows moves accordingly to the device horizontal axis.
15 |
16 |
17 |
18 |
19 | **Convex/Concave** shadow example:
20 |
21 |
22 | ```swift
23 | view.backgroundColor = systemBackground()
24 | let surface = SurfaceView()
25 | surface.frame = CGRect(x: 100, y: 100, width: 96, height: 32)
26 | surface.layer.cornerRadius = 16
27 | surface.surfaceLayer.shadow = Shadow(preset: .concave2)
28 | view.addSubview(surface)
29 |
30 | let button = SurfaceView()
31 | button.frame = CGRect(x: 0, y: 0, width: 48, height: 32)
32 | button.layer.cornerRadius = 16
33 | button.surfaceLayer.shadow = Shadow(preset: .convex1)
34 | button.backgroundColor = .white
35 | surface.addSubview(button)
36 | ```
37 |
38 |
39 |
--------------------------------------------------------------------------------
/Sources/Surface/CALayer+Animate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | // MARK: - Extension
4 |
5 | public extension CALayer {
6 | /// Animation wrapper on CALayer.
7 | func animate() -> _CALayerAnimate {
8 | _CALayerAnimate(layer: self)
9 | }
10 |
11 | func _isShadowAnimationOngoing() -> Bool {
12 | _containerRef._isShadowAnimationOngoing
13 | }
14 | }
15 |
16 | // MARK: - _CALayerAnimate
17 |
18 | @available(iOS 13.0, macOS 10.15, watchOS 6.0, tvOS 13.0, *)
19 | public class _CALayerAnimate {
20 | private var animations: [String: CAAnimation]
21 | private var duration: CFTimeInterval
22 | private let layer: CALayer
23 |
24 | init(layer: CALayer) {
25 | self.animations = [String: CAAnimation]()
26 | self.duration = 0.25 // second
27 | self.layer = layer
28 | }
29 |
30 | public func shadowOpacity(_ shadowOpacity: Float) -> _CALayerAnimate {
31 | let key = "shadowOpacity"
32 | let animation = CABasicAnimation(keyPath: key)
33 | animation.fromValue = layer.shadowOpacity
34 | animation.toValue = shadowOpacity
35 | animation.isRemovedOnCompletion = true
36 | animation.fillMode = CAMediaTimingFillMode.forwards
37 | animations[key] = animation
38 | return self
39 | }
40 |
41 | public func shadowRadius(_ shadowRadius: CGFloat) -> _CALayerAnimate {
42 | let key = "shadowRadius"
43 | let animation = CABasicAnimation(keyPath: key)
44 | animation.fromValue = layer.shadowRadius
45 | animation.toValue = shadowRadius
46 | animation.isRemovedOnCompletion = true
47 | animation.fillMode = CAMediaTimingFillMode.forwards
48 | animations[key] = animation
49 | return self
50 | }
51 |
52 | public func shadowOffset(_ size: CGSize) -> _CALayerAnimate {
53 | let key = "shadowOffset"
54 | let animation = CABasicAnimation(keyPath: key)
55 | animation.fromValue = NSValue(cgSize: layer.shadowOffset)
56 | animation.toValue = NSValue(cgSize: size)
57 | animation.isRemovedOnCompletion = true
58 | animation.fillMode = CAMediaTimingFillMode.forwards
59 |
60 | animations[key] = animation
61 | return self
62 | }
63 |
64 | public func duration(_ duration: CFTimeInterval) -> _CALayerAnimate {
65 | self.duration = duration
66 | return self
67 | }
68 |
69 | /// Apply the layer animation
70 | public func start() {
71 | layer._containerRef._isShadowAnimationOngoing = true
72 | CATransaction.begin()
73 | for (key, animation) in animations {
74 | animation.duration = duration
75 | layer.removeAnimation(forKey: key)
76 | layer.add(animation, forKey: key)
77 | }
78 | CATransaction.setCompletionBlock {
79 | self.layer._containerRef._isShadowAnimationOngoing = false
80 | }
81 | CATransaction.commit()
82 | }
83 | }
84 |
85 |
--------------------------------------------------------------------------------
/Sources/Surface/CALayer+Shadow.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import CoreMotion
3 |
4 | // MARK: - Extension
5 |
6 | public extension CALayer {
7 | /// The high-level shadow argument.
8 | var shadow: Shadow {
9 | get { _containerRef.shadow }
10 | set { _containerRef.shadow = newValue }
11 | }
12 | /// Whether it should use the device motion sensor to cast the shadow angle.
13 | var useDeviceMotionToCastShadow: Bool {
14 | get { _containerRef.useDeviceMotionToCastShadow }
15 | set { _containerRef.useDeviceMotionToCastShadow = newValue }
16 | }
17 |
18 | /// The layer shadow exposed as a canonical shadow format.
19 | /// - note: This triggers an implicit layer animation.
20 | var _appliedLayerShadow: CALayer._CanonicalShadowFormat {
21 | get { _containerRef._appliedLayerShadow }
22 | set { _containerRef._appliedLayerShadow = newValue }
23 | }
24 |
25 | /// Re-apply the shadow to this layer.
26 | func setNeedsLayoutShadow() {
27 | _containerRef.shadow = shadow
28 | }
29 |
30 | /// Updates the shadow path.
31 | func _layoutShadowPath() {
32 | guard _containerRef._isShadowPathAutoSizing else { return }
33 | if !_appliedLayerShadow.isVisible {
34 | shadowPath = nil
35 | } else {
36 | shadowPath = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius).cgPath
37 | }
38 | }
39 | }
40 |
41 | // MARK: _CALayerAssociatedContainer
42 |
43 | final class _CALayerAssociatedContainer {
44 | /// A reference to the CALayer.
45 | private weak var _layer: CALayer?
46 |
47 | /// The associated shadow argument.
48 | var shadow = Shadow(lightInterfaceStyleShadow: _ShadowPair(bottom: .zero)) {
49 | didSet {
50 | guard let layer = _layer else { return }
51 | shadow.applyToLayer(layer)
52 | layer.setNeedsLayout()
53 | layer.setNeedsDisplay()
54 | }
55 | }
56 |
57 | var useDeviceMotionToCastShadow: Bool = false {
58 | didSet {
59 | guard let layer = _layer else { return }
60 | if useDeviceMotionToCastShadow {
61 | _MotionManager.shared.registerLayer(layer)
62 | } else {
63 | _MotionManager.shared.deregisterLayer(layer)
64 | }
65 | }
66 | }
67 |
68 | var _isShadowAnimationOngoing: Bool = false
69 |
70 | /// Update the layer shadow animated.
71 | var _appliedLayerShadow: CALayer._CanonicalShadowFormat = .zero {
72 | didSet {
73 | guard let layer = _layer else { return }
74 | layer.animate()
75 | .shadowRadius(_appliedLayerShadow.blur / 2.0)
76 | .shadowOffset(CGSize(
77 | width: _appliedLayerShadow.offset.x,
78 | height: _appliedLayerShadow.offset.y))
79 | .shadowOpacity(Float(_appliedLayerShadow.alpha))
80 | .duration(0.166)
81 | .start()
82 | layer.shadowColor = _appliedLayerShadow.color.cgColor
83 | layer.shadowRadius = _appliedLayerShadow.blur / 2.0
84 | layer.shadowOpacity = Float(_appliedLayerShadow.alpha)
85 | layer.shadowOffset = CGSize(width: _appliedLayerShadow.offset.x, height: _appliedLayerShadow.offset.y)
86 | layer._layoutShadowPath()
87 | }
88 | }
89 |
90 | /// Enables automatic shadowPath sizing.
91 | var _isShadowPathAutoSizing = true
92 |
93 | init(layer: CALayer?) {
94 | self._layer = layer
95 | }
96 | }
97 |
98 | extension CALayer {
99 | /// Layer elevation/ animation state.
100 | var _containerRef: _CALayerAssociatedContainer {
101 | get {
102 | typealias C = _CALayerAssociatedContainer
103 | let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
104 | guard let obj = objc_getAssociatedObject(self, &_associatedContainerKey) as? C else {
105 | let container = _CALayerAssociatedContainer(layer: self)
106 | objc_setAssociatedObject(self, &_associatedContainerKey, container, nonatomic)
107 | return container
108 | }
109 | return obj
110 | }
111 | set(value) {
112 | let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC
113 | objc_setAssociatedObject(self, &_associatedContainerKey, value, nonatomic)
114 | }
115 | }
116 | }
117 |
118 | private var _associatedContainerKey: UInt8 = 0
119 |
--------------------------------------------------------------------------------
/Sources/Surface/MotionManager.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import CoreMotion
3 | import UIKit
4 |
5 | private let _sharedMotionManager: CMMotionManager = CMMotionManager()
6 |
7 | final class _MotionManager {
8 |
9 | private struct WeakLayerRef { weak var layer: CALayer? }
10 |
11 | static let shared = _MotionManager()
12 |
13 | private let _motionManager = CMMotionManager()
14 | private var _displayLink: CADisplayLink?
15 | private let _queue = OperationQueue()
16 | private var _layers: [WeakLayerRef] = []
17 | private var _angle: CGFloat = 1.0
18 |
19 | func registerLayer(_ layer: CALayer) {
20 | guard _layers.filter({ $0.layer === layer }).isEmpty else { return }
21 | _layers.append(WeakLayerRef(layer: layer))
22 | _startOrStopGyroUpdates()
23 | }
24 |
25 | func deregisterLayer(_ layer: CALayer) {
26 | _layers = _layers.filter { $0.layer != nil && $0.layer !== layer }
27 | _startOrStopGyroUpdates()
28 | }
29 |
30 | private func _startOrStopGyroUpdates() {
31 | _layers = _layers.filter { $0.layer != nil }
32 | if _layers.isEmpty {
33 | _motionManager.stopDeviceMotionUpdates()
34 | _displayLink = nil
35 | } else {
36 | guard _displayLink == nil else { return }
37 | _displayLink = CADisplayLink(target: self, selector: #selector(_onDisplayLinkFire))
38 | _displayLink?.add(to: .current, forMode: .default)
39 | _displayLink?.preferredFramesPerSecond = 10
40 | _motionManager.startDeviceMotionUpdates(to: _queue) { [weak self] data, error in
41 | guard let value = self?._motionManager.deviceMotion?.attitude.roll else { return }
42 | self?._angle = CGFloat(value)
43 | }
44 | }
45 | }
46 |
47 | @objc dynamic private func _onDisplayLinkFire() {
48 | let layers = _layers.compactMap { $0.layer }
49 | let angle = max(-1, min(1, _angle * 2))
50 | for layer in layers {
51 | guard !layer._isShadowAnimationOngoing() else { continue }
52 | layer.shadow = layer.shadow.withAngle(xt: angle, yt: 1)
53 | }
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/Surface/Neumorphism.swift:
--------------------------------------------------------------------------------
1 | // Forked from https://github.com/hirokimu/EMTNeumorphicView
2 | // Used internally as benchmark.
3 |
4 | // The MIT License (MIT)
5 | //
6 | // Copyright (c) 2020 Emotionale (https://www.emotionale.jp/)
7 | //
8 | // Permission is hereby granted, free of charge, to any person obtaining a copy
9 | // of this software and associated documentation files (the "Software"), to deal
10 | // in the Software without restriction, including without limitation the rights
11 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | // copies of the Software, and to permit persons to whom the Software is
13 | // furnished to do so, subject to the following conditions:
14 | //
15 | // The above copyright notice and this permission notice shall be included in all
16 | // copies or substantial portions of the Software.
17 | //
18 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | // SOFTWARE.
25 | //
26 |
27 | import UIKit
28 |
29 | /// `NeumorphicView` is a subclass of UIView and it provides some Neumorphism style design.
30 | /// Access neumorphicLayer. Change effects via its properties.
31 | public class NeumorphicView: UIView, NeumorphicElementProtocol {
32 | /// Change effects via its properties.
33 | public var neumorphicLayer: NeumorphicLayer? {
34 | return layer as? NeumorphicLayer
35 | }
36 | public override class var layerClass: AnyClass {
37 | return NeumorphicLayer.self
38 | }
39 | public override func layoutSubviews() {
40 | super.layoutSubviews()
41 | neumorphicLayer?.update()
42 | }
43 | }
44 |
45 | /// `NeumorphicButton` is a subclass of UIView and it provides some Neumorphism style design.
46 | /// Access neumorphicLayer. Change effects via its properties.
47 | public class NeumorphicButton: UIButton, NeumorphicElementProtocol {
48 | /// Change effects via its properties.
49 | public var neumorphicLayer: NeumorphicLayer? {
50 | return layer as? NeumorphicLayer
51 | }
52 | public override class var layerClass: AnyClass {
53 | return NeumorphicLayer.self
54 | }
55 | public override func layoutSubviews() {
56 | super.layoutSubviews()
57 | neumorphicLayer?.update()
58 | }
59 | public override var isHighlighted: Bool {
60 | didSet {
61 | if oldValue != isHighlighted {
62 | neumorphicLayer?.selected = isHighlighted
63 | }
64 | }
65 | }
66 | public override var isSelected: Bool {
67 | didSet {
68 | if oldValue != isSelected {
69 | neumorphicLayer?.depthType = isSelected ? .concave : .convex
70 | }
71 | }
72 | }
73 | }
74 |
75 | /// `NeumorphicTableCell` is a subclass of UITableViewCell and it provides some
76 | /// Neumorphism style design.
77 | /// Access neumorphicLayer. Change effects via its properties.
78 | public class NeumorphicTableCell: UITableViewCell, NeumorphicElementProtocol {
79 | private var _bg: NeumorphicView?
80 |
81 | override public init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
82 | super.init(style: style, reuseIdentifier: reuseIdentifier)
83 | backgroundColor = UIColor.clear
84 | }
85 |
86 | public required init?(coder: NSCoder) {
87 | fatalError("init(coder:) has not been implemented")
88 | }
89 | /// Change effects via its properties.
90 | public var neumorphicLayer: NeumorphicLayer? {
91 | if _bg == nil {
92 | _bg = NeumorphicView(frame: bounds)
93 | _bg?.neumorphicLayer?.owningView = self
94 | selectedBackgroundView = UIView()
95 | layer.masksToBounds = true
96 | backgroundView = _bg
97 | }
98 | return _bg?.neumorphicLayer
99 | }
100 | public override func layoutSubviews() {
101 | super.layoutSubviews()
102 | neumorphicLayer?.update()
103 | }
104 | public override func setHighlighted(_ highlighted: Bool, animated: Bool) {
105 | super.setHighlighted(highlighted, animated: animated)
106 | neumorphicLayer?.selected = highlighted
107 | }
108 | public override func setSelected(_ selected: Bool, animated: Bool) {
109 | super.setSelected(selected, animated: animated)
110 | neumorphicLayer?.selected = selected
111 | }
112 | public func depthTypeUpdated(to type: NeumorphicLayerDepthType) {
113 | if let l = _bg?.neumorphicLayer {
114 | layer.masksToBounds = l.depthType == .concave
115 | }
116 | }
117 | }
118 |
119 | public protocol NeumorphicElementProtocol : UIView {
120 | var neumorphicLayer: NeumorphicLayer? { get }
121 | func depthTypeUpdated(to type: NeumorphicLayerDepthType)
122 | }
123 |
124 | public extension NeumorphicElementProtocol {
125 | func depthTypeUpdated(to type: NeumorphicLayerDepthType) { }
126 | }
127 |
128 | public enum NeumorphicLayerCornerType: Int { case all, topRow, middleRow, bottomRow }
129 |
130 | public enum NeumorphicLayerDepthType: Int { case concave, convex }
131 |
132 | public class NeumorphicLayer: CALayer {
133 | private var _props: _NeumorphicLayerProps?
134 | public weak var owningView: NeumorphicElementProtocol?
135 |
136 | /// Default is 1.
137 | public var lightShadowOpacity: Float = 1 {
138 | didSet { if oldValue != lightShadowOpacity { setNeedsDisplay() } }
139 | }
140 |
141 | /// Default is 0.3.
142 | public var darkShadowOpacity: Float = 0.3 {
143 | didSet { if oldValue != darkShadowOpacity { setNeedsDisplay() } }
144 | }
145 |
146 | /// Optional. if it is nil (default), elementBackgroundColor will be used as element color.
147 | public var elementColor: CGColor? {
148 | didSet { if oldValue !== elementColor { setNeedsDisplay() } }
149 | }
150 |
151 | private var elementSelectedColor: CGColor?
152 |
153 | /// It will be used as base color for light/shadow.
154 | /// If elementColor is nil, elementBackgroundColor will be used as elementColor.
155 | public var elementBackgroundColor: CGColor = UIColor.white.cgColor {
156 | didSet { if oldValue !== elementBackgroundColor { setNeedsDisplay() } }
157 | }
158 |
159 | public var depthType: NeumorphicLayerDepthType = .convex {
160 | didSet {
161 | if oldValue != depthType {
162 | owningView?.depthTypeUpdated(to: depthType)
163 | setNeedsDisplay()
164 | }
165 | }
166 | }
167 |
168 | /// ".all" is for buttons. ".topRowm" ".middleRow" ".bottomRow" is for table cells.
169 | public var cornerType: NeumorphicLayerCornerType = .all {
170 | didSet { if oldValue != cornerType { setNeedsDisplay() } }
171 | }
172 |
173 | /// Default is 5.
174 | public var elementDepth: CGFloat = 5 {
175 | didSet { if oldValue != elementDepth { setNeedsDisplay() } }
176 | }
177 |
178 | /// Adding a very thin border on the edge of the element.
179 | public var edged: Bool = false {
180 | didSet { if oldValue != edged { setNeedsDisplay() } }
181 | }
182 |
183 | /// If set to true, show element highlight color. Animated.
184 | public var selected: Bool {
185 | get {
186 | return _selected
187 | }
188 | set {
189 | _selected = newValue
190 | let color = elementColor ?? elementBackgroundColor
191 | elementSelectedColor =
192 | UIColor(cgColor: color).getTransformedColor(saturation: 1, brightness: 0.9).cgColor
193 | _colorLayer?.backgroundColor = _selected ? elementSelectedColor : color
194 | }
195 | }
196 |
197 | private var _selected: Bool = false
198 | private var _colorLayer: CALayer?
199 | private var _shadowLayer: _ShadowLayer?
200 | private var _lightLayer: _ShadowLayer?
201 | private var _edgeLayer: _EdgeLayer?
202 | private var _darkSideColor: CGColor = UIColor.black.cgColor
203 | private var _lightSideColor: CGColor = UIColor.white.cgColor
204 |
205 |
206 | // MARK: Build Layers
207 |
208 | public override func display() {
209 | super.display()
210 | update()
211 | }
212 |
213 | public func update() {
214 | // check property update.
215 | let isBoundsUpdated: Bool = _colorLayer?.bounds != bounds
216 | var currentProps = _NeumorphicLayerProps()
217 | currentProps._cornerType = cornerType
218 | currentProps._depthType = depthType
219 | currentProps._edged = edged
220 | currentProps._lightShadowOpacity = lightShadowOpacity
221 | currentProps._darkShadowOpacity = darkShadowOpacity
222 | currentProps._elementColor = elementColor
223 | currentProps._elementBackgroundColor = elementBackgroundColor
224 | currentProps._elementDepth = elementDepth
225 | currentProps._cornerRadius = cornerRadius
226 | let isPropsNotChanged = _props == nil ? true : currentProps == _props!
227 | if !isBoundsUpdated && isPropsNotChanged {
228 | return
229 | }
230 | _props = currentProps
231 |
232 | // generate shadow color.
233 | let color = elementColor ?? elementBackgroundColor
234 | _lightSideColor = UIColor.white.cgColor
235 | _darkSideColor =
236 | UIColor(cgColor: elementBackgroundColor)
237 | .getTransformedColor(saturation: 0.1, brightness: 0).cgColor
238 |
239 | // add sublayers.
240 | if _colorLayer == nil {
241 | _colorLayer = CALayer()
242 | _colorLayer?.cornerCurve = .continuous
243 | _shadowLayer = _ShadowLayer()
244 | _lightLayer = _ShadowLayer()
245 | _edgeLayer = _EdgeLayer()
246 | insertSublayer(_edgeLayer!, at: 0)
247 | insertSublayer(_colorLayer!, at: 0)
248 | insertSublayer(_lightLayer!, at: 0)
249 | insertSublayer(_shadowLayer!, at: 0)
250 | }
251 | _colorLayer?.frame = bounds
252 | _colorLayer?.backgroundColor = _selected ? elementSelectedColor : color
253 | if depthType == .convex {
254 | masksToBounds = false
255 | _colorLayer?.removeFromSuperlayer()
256 | insertSublayer(_colorLayer!, at: 2)
257 | _colorLayer?.masksToBounds = true
258 | _shadowLayer?.masksToBounds = false
259 | _lightLayer?.masksToBounds = false
260 | _edgeLayer?.masksToBounds = false
261 | }
262 | else {
263 | masksToBounds = true
264 | _colorLayer?.removeFromSuperlayer()
265 | insertSublayer(_colorLayer!, at: 0)
266 | _colorLayer?.masksToBounds = true
267 | _shadowLayer?.masksToBounds = true
268 | _lightLayer?.masksToBounds = true
269 | _edgeLayer?.masksToBounds = true
270 | }
271 |
272 | // initialize sublayers.
273 | _shadowLayer?.initialize(
274 | bounds: bounds, mode: .darkSide, props: _props!, color: _darkSideColor)
275 | _lightLayer?.initialize(
276 | bounds: bounds, mode: .lightSide, props: _props!, color: _lightSideColor)
277 |
278 | if currentProps._edged {
279 | _edgeLayer?.initialize(bounds: bounds, props: _props!, color: _lightSideColor)
280 | }
281 | else {
282 | _edgeLayer?.reset()
283 | }
284 |
285 | // set corners and outer mask.
286 | switch cornerType {
287 | case .all:
288 | if depthType == .convex {
289 | _colorLayer?.cornerRadius = cornerRadius
290 | }
291 | case .topRow:
292 | maskedCorners = [.layerMaxXMinYCorner, .layerMinXMinYCorner]
293 | if depthType == .convex {
294 | _colorLayer?.cornerRadius = cornerRadius
295 | _colorLayer?.maskedCorners = maskedCorners
296 | applyOuterMask(bounds: bounds, props: _props!)
297 | }
298 | else {
299 | mask = nil
300 | }
301 | case .middleRow:
302 | maskedCorners = []
303 | if depthType == .convex {
304 | applyOuterMask(bounds: bounds, props: _props!)
305 | }
306 | else {
307 | mask = nil
308 | }
309 | case .bottomRow:
310 | maskedCorners = [.layerMaxXMaxYCorner, .layerMinXMaxYCorner]
311 | if depthType == .convex {
312 | _colorLayer?.cornerRadius = cornerRadius
313 | _colorLayer?.maskedCorners = maskedCorners
314 | applyOuterMask(bounds: bounds, props: _props!)
315 | }
316 | else {
317 | mask = nil
318 | }
319 | }
320 | }
321 |
322 | private func applyOuterMask(bounds: CGRect, props: _NeumorphicLayerProps) {
323 | let shadowRadius = props._elementDepth
324 | let extendWidth = shadowRadius * 2
325 | var maskFrame = CGRect()
326 | switch props._cornerType {
327 | case .all:
328 | return
329 | case .topRow:
330 | maskFrame = CGRect(
331 | x: -extendWidth,
332 | y: -extendWidth,
333 | width: bounds.size.width + extendWidth * 2,
334 | height: bounds.size.height + extendWidth)
335 | case .middleRow:
336 | maskFrame = CGRect(
337 | x: -extendWidth,
338 | y: 0,
339 | width: bounds.size.width + extendWidth * 2,
340 | height: bounds.size.height)
341 | case .bottomRow:
342 | maskFrame = CGRect(
343 | x: -extendWidth,
344 | y: 0,
345 | width: bounds.size.width + extendWidth * 2,
346 | height: bounds.size.height + extendWidth)
347 | }
348 | let maskLayer = CALayer()
349 | maskLayer.frame = maskFrame
350 | maskLayer.backgroundColor = UIColor.white.cgColor
351 | mask = maskLayer
352 | }
353 | }
354 |
355 | // MARK - Private
356 |
357 | fileprivate struct _NeumorphicLayerProps {
358 | var _lightShadowOpacity: Float = 1
359 | var _darkShadowOpacity: Float = 0.3
360 | var _elementColor: CGColor?
361 | var _elementBackgroundColor: CGColor = UIColor.white.cgColor
362 | var _depthType: NeumorphicLayerDepthType = .convex
363 | var _cornerType: NeumorphicLayerCornerType = .all
364 | var _elementDepth: CGFloat = 5
365 | var _edged: Bool = false
366 | var _cornerRadius: CGFloat = 0
367 |
368 | static func == (lhs: _NeumorphicLayerProps, rhs: _NeumorphicLayerProps) -> Bool {
369 | return lhs._lightShadowOpacity == rhs._lightShadowOpacity &&
370 | lhs._darkShadowOpacity == rhs._darkShadowOpacity &&
371 | lhs._elementColor === rhs._elementColor &&
372 | lhs._elementBackgroundColor === rhs._elementBackgroundColor &&
373 | lhs._depthType == rhs._depthType &&
374 | lhs._cornerType == rhs._cornerType &&
375 | lhs._elementDepth == rhs._elementDepth &&
376 | lhs._edged == rhs._edged &&
377 | lhs._cornerRadius == rhs._cornerRadius
378 | }
379 | }
380 |
381 | fileprivate enum _ShadowLayerMode: Int { case lightSide, darkSide }
382 |
383 | fileprivate class _ShadowLayerBase: CALayer {
384 | static let corners: [NeumorphicLayerCornerType: UIRectCorner] = [
385 | .all: [.topLeft, .topRight, .bottomLeft, .bottomRight],
386 | .topRow: [.topLeft, .topRight],
387 | .middleRow: [],
388 | .bottomRow: [.bottomLeft, .bottomRight]
389 | ]
390 | func setCorner(props: _NeumorphicLayerProps) {
391 | switch props._cornerType {
392 | case .all:
393 | cornerRadius = props._cornerRadius
394 | maskedCorners = [
395 | .layerMinXMinYCorner,
396 | .layerMaxXMinYCorner,
397 | .layerMinXMaxYCorner,
398 | .layerMaxXMaxYCorner]
399 | case .topRow:
400 | cornerRadius = props._cornerRadius
401 | maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
402 | case .middleRow:
403 | cornerRadius = 0
404 | maskedCorners = []
405 | case .bottomRow:
406 | cornerRadius = props._cornerRadius
407 | maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
408 | }
409 | }
410 | }
411 |
412 | fileprivate class _ShadowLayer: _ShadowLayerBase {
413 | private var _lightLayer: CALayer?
414 |
415 | func initialize(
416 | bounds: CGRect,
417 | mode: _ShadowLayerMode,
418 | props: _NeumorphicLayerProps,
419 | color: CGColor
420 | ) {
421 | cornerCurve = .continuous
422 | shouldRasterize = true
423 | rasterizationScale = UIScreen.main.scale
424 | if props._depthType == .convex {
425 | applyOuterShadow(bounds: bounds, mode: mode, props: props, color: color)
426 | }
427 | else { // .concave
428 | applyInnerShadow(bounds: bounds, mode: mode, props: props, color: color)
429 | }
430 | }
431 |
432 | func applyOuterShadow(
433 | bounds: CGRect,
434 | mode: _ShadowLayerMode,
435 | props: _NeumorphicLayerProps,
436 | color: CGColor
437 | ) {
438 | _lightLayer?.removeFromSuperlayer()
439 | _lightLayer = nil
440 |
441 | frame = bounds
442 | cornerRadius = 0
443 | maskedCorners = []
444 | masksToBounds = false
445 | mask = nil
446 |
447 | let shadowCornerRadius = props._cornerType == .middleRow ? 0 : props._cornerRadius
448 |
449 | // prepare shadow parameters.
450 | let shadowRadius = props._elementDepth
451 | let offsetWidth: CGFloat = shadowRadius / 2
452 | let cornerRadii: CGSize = props._cornerRadius <= 0
453 | ? CGSize.zero
454 | : CGSize(width: shadowCornerRadius - offsetWidth, height: shadowCornerRadius - offsetWidth)
455 |
456 | var shadowX: CGFloat = 0
457 | var shadowY: CGFloat = 0
458 | if mode == .lightSide {
459 | shadowY = -offsetWidth
460 | shadowX = -offsetWidth
461 | }
462 | else {
463 | shadowY = offsetWidth
464 | shadowX = offsetWidth
465 | }
466 |
467 | setCorner(props: props)
468 | let corners = _ShadowLayer.corners[props._cornerType]!
469 | let extendHeight = max(props._cornerRadius, shadowCornerRadius)
470 |
471 | // add shadow.
472 | var shadowBounds = bounds
473 | switch props._cornerType {
474 | case .all:
475 | break
476 | case .topRow:
477 | shadowBounds = CGRect(
478 | x: bounds.origin.x,
479 | y: bounds.origin.y,
480 | width: bounds.size.width,
481 | height: bounds.size.height + extendHeight)
482 | case .middleRow:
483 | shadowY = 0
484 | shadowBounds = CGRect(
485 | x: bounds.origin.x,
486 | y: bounds.origin.y - extendHeight,
487 | width: bounds.size.width,
488 | height: bounds.size.height + extendHeight * 2)
489 | case .bottomRow:
490 | shadowBounds = CGRect(
491 | x: bounds.origin.x,
492 | y: bounds.origin.y - extendHeight,
493 | width: bounds.size.width,
494 | height: bounds.size.height + extendHeight)
495 | }
496 |
497 | let path: UIBezierPath = UIBezierPath(
498 | roundedRect: shadowBounds.insetBy(dx: offsetWidth, dy: offsetWidth),
499 | byRoundingCorners: corners,
500 | cornerRadii: cornerRadii)
501 | shadowPath = path.cgPath
502 | shadowColor = color
503 | shadowOffset = CGSize(width: shadowX, height: shadowY)
504 | shadowOpacity = mode == .lightSide ? props._lightShadowOpacity : props._darkShadowOpacity
505 | self.shadowRadius = shadowRadius
506 | }
507 |
508 | func applyInnerShadow(
509 | bounds: CGRect,
510 | mode: _ShadowLayerMode,
511 | props: _NeumorphicLayerProps,
512 | color: CGColor
513 | ) {
514 | let width = bounds.size.width
515 | let height = bounds.size.height
516 | frame = bounds
517 |
518 | // prepare shadow parameters
519 | let shadowRadius = props._elementDepth * 0.75
520 |
521 | let gap: CGFloat = 1
522 |
523 | let cornerRadii: CGSize = CGSize(
524 | width: props._cornerRadius + gap, height: props._cornerRadius + gap)
525 | let cornerRadiusInner = props._cornerRadius - gap
526 | let cornerRadiiInner: CGSize = CGSize(
527 | width: cornerRadiusInner, height: cornerRadiusInner)
528 | var shadowX: CGFloat = 0
529 | var shadowY: CGFloat = 0
530 | var shadowWidth: CGFloat = width
531 | var shadowHeight: CGFloat = height
532 |
533 | setCorner(props: props)
534 | let corners = _ShadowLayer.corners[props._cornerType]!
535 | switch props._cornerType {
536 | case .all:
537 | break
538 | case .topRow:
539 | shadowHeight += shadowRadius * 4
540 | case .middleRow:
541 | if mode == .lightSide {
542 | shadowWidth += shadowRadius * 3
543 | shadowHeight += shadowRadius * 6
544 | shadowY = -(shadowRadius * 3)
545 | shadowX = -(shadowRadius * 3)
546 | }
547 | else {
548 | shadowWidth += shadowRadius * 2
549 | shadowHeight += shadowRadius * 6
550 | shadowY -= (shadowRadius * 3)
551 | }
552 | case .bottomRow:
553 | shadowHeight += shadowRadius * 4
554 | shadowY = -shadowRadius * 4
555 | }
556 |
557 | // add shadow
558 | let shadowBounds = CGRect(x: 0, y: 0, width: shadowWidth, height: shadowHeight)
559 | var path: UIBezierPath
560 | var innerPath: UIBezierPath
561 |
562 | if props._cornerType == .middleRow {
563 | path = UIBezierPath(rect: shadowBounds.insetBy(dx: -gap, dy: -gap))
564 | innerPath = UIBezierPath(rect: shadowBounds.insetBy(dx: gap, dy: gap)).reversing()
565 | }
566 | else {
567 | path = UIBezierPath(roundedRect:shadowBounds.insetBy(dx: -gap, dy: -gap),
568 | byRoundingCorners: corners,
569 | cornerRadii: cornerRadii)
570 | innerPath = UIBezierPath(roundedRect: shadowBounds.insetBy(dx: gap, dy: gap),
571 | byRoundingCorners: corners,
572 | cornerRadii: cornerRadiiInner).reversing()
573 | }
574 | path.append(innerPath)
575 |
576 | shadowPath = path.cgPath
577 | masksToBounds = true
578 | shadowColor = color
579 | shadowOffset = CGSize(width: shadowX, height: shadowY)
580 | shadowOpacity = mode == .lightSide ? props._lightShadowOpacity : props._darkShadowOpacity
581 | self.shadowRadius = shadowRadius
582 |
583 | if mode == .lightSide {
584 | if _lightLayer == nil {
585 | _lightLayer = CALayer()
586 | addSublayer(_lightLayer!)
587 | }
588 | _lightLayer?.frame = bounds
589 | _lightLayer?.shadowPath = path.cgPath
590 | _lightLayer?.masksToBounds = true
591 | _lightLayer?.shadowColor = shadowColor
592 | _lightLayer?.shadowOffset = CGSize(width: shadowX, height: shadowY)
593 | _lightLayer?.shadowOpacity = props._lightShadowOpacity
594 | _lightLayer?.shadowRadius = shadowRadius
595 | _lightLayer?.shouldRasterize = true
596 | }
597 |
598 | // add mask to shadow.
599 | if props._cornerType == .middleRow {
600 | mask = nil
601 | }
602 | else {
603 | let maskLayer = _GradientMaskLayer()
604 | maskLayer.frame = bounds
605 | maskLayer.cornerType = props._cornerType
606 | maskLayer.shadowLayerMode = mode
607 | maskLayer.shadowCornerRadius = props._cornerRadius
608 | mask = maskLayer
609 | }
610 | }
611 | }
612 |
613 | fileprivate class _EdgeLayer: _ShadowLayerBase {
614 | func initialize(bounds: CGRect, props: _NeumorphicLayerProps, color: CGColor) {
615 |
616 | setCorner(props: props)
617 | let corners = _EdgeLayer.corners[props._cornerType]!
618 |
619 | cornerCurve = .continuous
620 | shouldRasterize = true
621 | frame = bounds
622 |
623 | var shadowY: CGFloat = 0
624 | var path: UIBezierPath
625 | var innerPath: UIBezierPath
626 | let edgeWidth: CGFloat = 0.75
627 |
628 | var edgeBounds = bounds
629 | let cornerRadii: CGSize = CGSize(width: props._cornerRadius, height: props._cornerRadius)
630 | let cornerRadiusEdge = props._cornerRadius - edgeWidth
631 | let cornerRadiiEdge: CGSize = CGSize(width: cornerRadiusEdge, height: cornerRadiusEdge)
632 |
633 | if props._depthType == .convex {
634 |
635 | switch props._cornerType {
636 | case .all:
637 | break
638 | case .topRow:
639 | edgeBounds = CGRect(
640 | x: bounds.origin.x,
641 | y: bounds.origin.y,
642 | width: bounds.size.width,
643 | height: bounds.size.height + 2)
644 | case .middleRow:
645 | edgeBounds = CGRect(
646 | x: bounds.origin.x,
647 | y: bounds.origin.y - 2,
648 | width: bounds.size.width,
649 | height: bounds.size.height + 4)
650 | case .bottomRow:
651 | edgeBounds = CGRect(
652 | x: bounds.origin.x,
653 | y: bounds.origin.y - 2,
654 | width: bounds.size.width,
655 | height: bounds.size.height + 2)
656 | }
657 |
658 | path = UIBezierPath(
659 | roundedRect: edgeBounds, byRoundingCorners: corners, cornerRadii: cornerRadii)
660 | let innerPath = UIBezierPath(
661 | roundedRect: edgeBounds.insetBy(dx: edgeWidth, dy: edgeWidth),
662 | byRoundingCorners: corners, cornerRadii: cornerRadiiEdge).reversing()
663 | path.append(innerPath)
664 | shadowPath = path.cgPath
665 | shadowColor = color
666 | shadowOffset = CGSize.zero
667 | shadowOpacity = min(props._lightShadowOpacity * 1.5, 1)
668 | shadowRadius = 0
669 | }
670 | else {
671 | // shadow size and y position.
672 | if props._depthType == .concave {
673 | switch props._cornerType {
674 | case .all:
675 | break
676 | case .topRow:
677 | edgeBounds.size.height += 2
678 | case .middleRow:
679 | shadowY = -5
680 | edgeBounds.size.height += 10
681 | case .bottomRow:
682 | shadowY = -2
683 | edgeBounds.size.height += 2
684 | }
685 | }
686 | // shadow path.
687 | if props._cornerType == .middleRow {
688 | path = UIBezierPath(rect: edgeBounds)
689 | innerPath = UIBezierPath(
690 | rect: edgeBounds.insetBy(dx: edgeWidth, dy: edgeWidth)).reversing()
691 | }
692 | else {
693 | path = UIBezierPath(
694 | roundedRect: edgeBounds, byRoundingCorners: corners, cornerRadii: cornerRadii)
695 | innerPath = UIBezierPath(
696 | roundedRect: edgeBounds.insetBy(dx: edgeWidth, dy: edgeWidth),
697 | byRoundingCorners: corners, cornerRadii: cornerRadiiEdge).reversing()
698 | }
699 | path.append(innerPath)
700 | shadowPath = path.cgPath
701 | shadowColor = color
702 | shadowOffset = CGSize(width: 0, height: shadowY)
703 | shadowOpacity = min(props._lightShadowOpacity * 1.5, 1)
704 | shadowRadius = 0
705 | }
706 | }
707 | func reset() {
708 | shadowPath = nil
709 | shadowOffset = CGSize.zero
710 | shadowOpacity = 0
711 | frame = CGRect()
712 | }
713 | }
714 |
715 | fileprivate class _GradientMaskLayer: CALayer {
716 | required override init() {
717 | super.init()
718 | needsDisplayOnBoundsChange = true
719 | }
720 | required init?(coder aDecoder: NSCoder) {
721 | super.init(coder: aDecoder)
722 | }
723 | required override init(layer: Any) {
724 | super.init(layer: layer)
725 | }
726 |
727 | var cornerType: NeumorphicLayerCornerType = .all
728 | var shadowLayerMode: _ShadowLayerMode = .lightSide
729 | var shadowCornerRadius: CGFloat = 0
730 |
731 | private func getTopRightCornerRect(size: CGSize, radius: CGFloat) -> CGRect {
732 | return CGRect(x: size.width - radius, y: 0, width: radius, height: radius)
733 | }
734 | private func getBottomLeftCornerRect(size: CGSize, radius: CGFloat) -> CGRect {
735 | return CGRect(x: 0, y: size.height - radius, width: radius, height: radius)
736 | }
737 |
738 | override func draw(in ctx: CGContext) {
739 | let rectTR = getTopRightCornerRect(size: frame.size, radius: shadowCornerRadius)
740 | let rectTR_BR = CGPoint(x: rectTR.maxX, y: rectTR.maxY)
741 | let rectBL = getBottomLeftCornerRect(size: frame.size, radius: shadowCornerRadius)
742 | let rectBL_BR = CGPoint(x: rectBL.maxX, y: rectBL.maxY)
743 |
744 | let color = UIColor.black.cgColor
745 |
746 | guard let gradient = CGGradient(
747 | colorsSpace: CGColorSpaceCreateDeviceRGB(),
748 | colors: [color, UIColor.clear.cgColor] as CFArray,
749 | locations: [0, 1]) else { return }
750 |
751 | if cornerType == .all {
752 | if shadowLayerMode == .lightSide {
753 | if frame.size.width > shadowCornerRadius * 2 && frame.size.height > shadowCornerRadius * 2 {
754 | ctx.setFillColor(color)
755 | ctx.fill(CGRect(
756 | x: shadowCornerRadius,
757 | y: shadowCornerRadius,
758 | width: frame.size.width - shadowCornerRadius,
759 | height: frame.size.height - shadowCornerRadius)
760 | )
761 | }
762 | ctx.saveGState()
763 | ctx.addRect(rectTR)
764 | ctx.clip()
765 | ctx.drawLinearGradient(gradient, start: rectTR_BR, end: rectTR.origin, options: [])
766 | ctx.restoreGState()
767 | ctx.saveGState()
768 | ctx.addRect(rectBL)
769 | ctx.clip()
770 | ctx.drawLinearGradient(gradient, start: rectBL_BR, end: rectBL.origin, options: [])
771 | ctx.restoreGState()
772 | }
773 | else {
774 | if frame.size.width > shadowCornerRadius * 2 && frame.size.height > shadowCornerRadius * 2 {
775 | ctx.setFillColor(color)
776 | ctx.fill(CGRect(
777 | x: 0,
778 | y: 0,
779 | width: frame.size.width - shadowCornerRadius,
780 | height: frame.size.height - shadowCornerRadius)
781 | )
782 | }
783 | ctx.saveGState()
784 | ctx.addRect(rectTR)
785 | ctx.clip()
786 | ctx.drawLinearGradient(gradient, start: rectTR.origin, end: rectTR_BR, options: [])
787 | ctx.restoreGState()
788 | ctx.saveGState()
789 | ctx.addRect(rectBL)
790 | ctx.clip()
791 | ctx.drawLinearGradient(gradient, start: rectBL.origin, end: rectBL_BR, options: [])
792 | ctx.restoreGState()
793 | }
794 | }
795 | else if cornerType == .topRow {
796 | if shadowLayerMode == .lightSide {
797 | ctx.setFillColor(color)
798 | ctx.fill(CGRect(
799 | x: frame.size.width - shadowCornerRadius,
800 | y: shadowCornerRadius,
801 | width: frame.size.width,
802 | height: frame.size.height - shadowCornerRadius)
803 | )
804 | ctx.saveGState()
805 | ctx.addRect(rectTR)
806 | ctx.clip()
807 | ctx.drawLinearGradient(gradient, start: rectTR_BR, end: rectTR.origin, options: [])
808 | ctx.restoreGState()
809 | }
810 | else {
811 | ctx.setFillColor(color)
812 | ctx.fill(CGRect(
813 | x: 0,
814 | y: 0,
815 | width: frame.size.width - shadowCornerRadius,
816 | height: frame.size.height)
817 | )
818 | ctx.saveGState()
819 | ctx.addRect(rectTR)
820 | ctx.clip()
821 | ctx.drawLinearGradient(gradient, start: rectTR.origin, end: rectTR_BR, options: [])
822 | ctx.restoreGState()
823 | }
824 | }
825 | else if cornerType == .bottomRow {
826 | ctx.setFillColor(color)
827 | if shadowLayerMode == .lightSide {
828 | ctx.fill(CGRect(
829 | x: shadowCornerRadius,
830 | y: 0,
831 | width: frame.size.width - shadowCornerRadius,
832 | height: frame.size.height)
833 | )
834 | ctx.saveGState()
835 | ctx.addRect(rectBL)
836 | ctx.clip()
837 | ctx.drawLinearGradient(gradient, start: rectBL_BR, end: rectBL.origin, options: [])
838 | ctx.restoreGState()
839 | }
840 | else {
841 | ctx.fill(CGRect(
842 | x: 0,
843 | y: 0,
844 | width: shadowCornerRadius,
845 | height: frame.size.height - shadowCornerRadius)
846 | )
847 | ctx.saveGState()
848 | ctx.addRect(rectBL)
849 | ctx.clip()
850 | ctx.drawLinearGradient(gradient, start: rectBL.origin, end: rectBL_BR, options: [])
851 | ctx.restoreGState()
852 | }
853 | }
854 | }
855 | }
856 |
857 | // MARK - Extension
858 |
859 | extension UIColor {
860 | public convenience init(RGB: Int) {
861 | var rgb = RGB
862 | rgb = rgb > 0xffffff ? 0xffffff : rgb
863 | let r = CGFloat(rgb >> 16) / 255.0
864 | let g = CGFloat(rgb >> 8 & 0x00ff) / 255.0
865 | let b = CGFloat(rgb & 0x0000ff) / 255.0
866 | self.init(red: r, green: g, blue: b, alpha: 1.0)
867 | }
868 | public func getTransformedColor(saturation: CGFloat, brightness: CGFloat) -> UIColor {
869 | var hsb = getHSBColor()
870 | hsb.s *= saturation
871 | hsb.b *= brightness
872 | if hsb.s > 1 { hsb.s = 1 }
873 | if hsb.b > 1 { hsb.b = 1 }
874 | return hsb.uiColor
875 | }
876 | private func getHSBColor() -> HSBColor {
877 | var h: CGFloat = 0
878 | var s: CGFloat = 0
879 | var b: CGFloat = 0
880 | var a: CGFloat = 0
881 | getHue(&h, saturation: &s, brightness: &b, alpha: &a)
882 | return HSBColor(h: h, s: s, b: b, alpha: a)
883 | }
884 | }
885 |
886 | private struct HSBColor {
887 | var h: CGFloat
888 | var s: CGFloat
889 | var b: CGFloat
890 | var alpha: CGFloat
891 | init(h: CGFloat, s: CGFloat, b: CGFloat, alpha: CGFloat) {
892 | self.h = h
893 | self.s = s
894 | self.b = b
895 | self.alpha = alpha
896 | }
897 | var uiColor: UIColor {
898 | get {
899 | return UIColor(hue: h, saturation: s, brightness: b, alpha: alpha)
900 | }
901 | }
902 | }
903 |
--------------------------------------------------------------------------------
/Sources/Surface/Presets.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public enum DepthPreset {
4 | case concave5
5 | case concave4
6 | case concave3
7 | case concave2
8 | case concave1
9 | case none
10 | case convex1
11 | case convex2
12 | case convex3
13 | case convex4
14 | case convex5
15 |
16 | /// Blur value for this depth.
17 | public var blur: CGFloat {
18 | switch self {
19 | case .concave5: return 16
20 | case .concave4: return 8
21 | case .concave3: return 4
22 | case .concave2: return 2
23 | case .concave1: return 1
24 | case .none: return 0
25 | case .convex1: return 1
26 | case .convex2: return 2
27 | case .convex3: return 4
28 | case .convex4: return 8
29 | case .convex5: return 16
30 | }
31 | }
32 |
33 | /// Offset value for this depth.
34 | public var offset: CGFloat {
35 | switch self {
36 | case .concave5: return -8
37 | case .concave4: return -4
38 | case .concave3: return -2
39 | case .concave2: return -1
40 | case .concave1: return -0.5
41 | case .none: return 0
42 | case .convex1: return 0.5
43 | case .convex2: return 1
44 | case .convex3: return 2
45 | case .convex4: return 4
46 | case .convex5: return 8
47 | }
48 | }
49 | }
50 |
51 | public struct Defaults {
52 | public struct Light {
53 | static var systemBackground = UIColor(red:0.90, green:0.90, blue:0.90, alpha:1.0)
54 | static var background = UIColor(red:0.93, green:0.93, blue:0.93, alpha:1.0)
55 | static var bottomShadowOpacity: CGFloat = 0.16
56 | static var topShadowOpacity: CGFloat = 0.5
57 | static var bottomShadowTint: UIColor = .black
58 | static var topShadowTint: UIColor = .white
59 | }
60 | public struct Dark {
61 | static var systemBackground = UIColor(red:0.13, green:0.13, blue:0.15, alpha:1.0)
62 | static var background = UIColor(red:0.10, green:0.10, blue:0.10, alpha:1.0)
63 | static var bottomShadowOpacity: CGFloat = 0.18
64 | static var topShadowOpacity: CGFloat = 0.7
65 | static var bottomShadowTint: UIColor = .black
66 | static var topShadowTint: UIColor = UIColor(red:0.20, green:0.20, blue:0.20, alpha:1.0)
67 | }
68 | }
69 |
70 | /// Default system background color.
71 | public func systemBackground(shouldReactToIntefaceStyleChange: Bool = true) -> UIColor {
72 | let isDarkAppearance =
73 | UIScreen.main.traitCollection.userInterfaceStyle == .dark && shouldReactToIntefaceStyleChange
74 | return isDarkAppearance ? Defaults.Dark.systemBackground : Defaults.Light.systemBackground
75 | }
76 |
--------------------------------------------------------------------------------
/Sources/Surface/Shadow.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | public struct Shadow {
4 | /// Default shadow descriptor.
5 | public let lightInterfaceStyleShadow: _ShadowPair
6 | /// Dark shadow descriptor.
7 | public let darkInterfaceStyleShadow: _ShadowPair
8 | /// Whether the shadow should change when the appearance changed.
9 | public let shouldReactToIntefaceStyleChange: Bool
10 | /// Whether the overlay (highlight) shadow should be applied.
11 | public let shouldUseOverlayShadow: Bool
12 | /// Optional — Whether the view surface should be tinted accordingly to the shadow.
13 | public let shouldApplySurfaceBackground: Bool
14 | /// Optional — This dynamically controls background color of the view.
15 | public let lightSurfaceBackground: UIColor?
16 | /// Optional — This dynamically controls background color of the view.
17 | public let darkSurfaceBackground: UIColor?
18 |
19 | private var _horizontalCastAngle: CGFloat = 0
20 | private var _verticalCastAngle: CGFloat = 1
21 | private var _preset: DepthPreset?
22 |
23 | public init(
24 | lightInterfaceStyleShadow: _ShadowPair,
25 | darkInterfaceStyleShadow: _ShadowPair? = nil,
26 | shouldReactToIntefaceStyleChange: Bool = true,
27 | shouldUseOverlayShadow: Bool = true,
28 | shouldApplySurfaceBackground: Bool = true,
29 | lightSurfaceBackground: UIColor? = nil,
30 | darkSurfaceBackground: UIColor? = nil
31 | ) {
32 | self.lightInterfaceStyleShadow = lightInterfaceStyleShadow
33 | self.darkInterfaceStyleShadow = darkInterfaceStyleShadow ?? lightInterfaceStyleShadow
34 | self.shouldReactToIntefaceStyleChange = shouldReactToIntefaceStyleChange
35 | self.shouldUseOverlayShadow = shouldUseOverlayShadow
36 | self.lightSurfaceBackground = lightSurfaceBackground
37 | self.darkSurfaceBackground = darkSurfaceBackground
38 | self.shouldApplySurfaceBackground = shouldApplySurfaceBackground
39 | }
40 |
41 | public init(
42 | preset: DepthPreset,
43 | horizontalCastAngle: CGFloat = 0,
44 | verticalCastAngle: CGFloat = 1,
45 | shouldReactToIntefaceStyleChange: Bool = true,
46 | shouldUseOverlayShadow: Bool = true,
47 | shouldApplySurfaceBackground: Bool = true
48 | ) {
49 | let xt = horizontalCastAngle
50 | let yt = verticalCastAngle
51 | let lightBottomShadow = CALayer._CanonicalShadowFormat(
52 | color: Defaults.Light.bottomShadowTint,
53 | alpha: Defaults.Light.bottomShadowOpacity,
54 | offset: CGPoint(x: xt * preset.offset, y: yt * preset.offset),
55 | blur: preset.blur,
56 | spread: 1)
57 | let lightTopShadow = CALayer._CanonicalShadowFormat(
58 | color: Defaults.Light.topShadowTint,
59 | alpha: Defaults.Light.topShadowOpacity,
60 | offset: CGPoint(x: -xt * preset.offset, y: -yt * preset.offset),
61 | blur: preset.blur,
62 | spread: 1)
63 | let darkBottomShadow = CALayer._CanonicalShadowFormat(
64 | color: Defaults.Dark.bottomShadowTint,
65 | alpha: Defaults.Dark.bottomShadowOpacity,
66 | offset: CGPoint(x: xt * preset.offset, y: yt * preset.offset),
67 | blur: preset.blur,
68 | spread: 1)
69 | let darkTopShadow = CALayer._CanonicalShadowFormat(
70 | color: Defaults.Dark.topShadowTint,
71 | alpha: Defaults.Dark.topShadowOpacity,
72 | offset: CGPoint(x: -xt * preset.offset, y: -yt * preset.offset),
73 | blur: preset.blur,
74 | spread: 1)
75 |
76 | let lightInterfaceStyleShadow = _ShadowPair(
77 | bottom: lightBottomShadow,
78 | top: lightTopShadow)
79 | let darkInterfaceStyleShadow = _ShadowPair(
80 | bottom: darkBottomShadow,
81 | top: darkTopShadow)
82 |
83 | self.init(
84 | lightInterfaceStyleShadow: lightInterfaceStyleShadow,
85 | darkInterfaceStyleShadow: darkInterfaceStyleShadow,
86 | shouldReactToIntefaceStyleChange: shouldReactToIntefaceStyleChange,
87 | shouldUseOverlayShadow: shouldUseOverlayShadow,
88 | lightSurfaceBackground: shouldApplySurfaceBackground ? Defaults.Light.background : nil,
89 | darkSurfaceBackground: shouldApplySurfaceBackground ? Defaults.Dark.background : nil)
90 | _preset = preset
91 | _horizontalCastAngle = xt
92 | _verticalCastAngle = yt
93 | }
94 |
95 | public func applyToLayer(_ layer: CALayer) {
96 | let isDarkAppearance =
97 | UIScreen.main.traitCollection.userInterfaceStyle == .dark && shouldReactToIntefaceStyleChange
98 | let background = isDarkAppearance ? darkSurfaceBackground : lightSurfaceBackground
99 | let shadow = isDarkAppearance ? darkInterfaceStyleShadow : lightInterfaceStyleShadow
100 |
101 | layer._appliedLayerShadow = shadow.bottom
102 | if shouldUseOverlayShadow, let layer = layer as? SurfaceLayer, let top = shadow.top {
103 | layer._appliedOverlayLayerShadow = top
104 | }
105 | if shouldApplySurfaceBackground {
106 | layer.backgroundColor = background?.cgColor
107 | }
108 | }
109 |
110 | public func withAngle(xt: CGFloat, yt: CGFloat) -> Self {
111 | guard let preset = _preset else { return self }
112 | return Shadow(
113 | preset: preset,
114 | horizontalCastAngle: xt,
115 | verticalCastAngle: yt,
116 | shouldReactToIntefaceStyleChange: shouldReactToIntefaceStyleChange,
117 | shouldUseOverlayShadow: shouldUseOverlayShadow,
118 | shouldApplySurfaceBackground: shouldApplySurfaceBackground)
119 | }
120 |
121 | public func withPreset(_ preset: DepthPreset) -> Self {
122 | return Shadow(
123 | preset: preset,
124 | horizontalCastAngle: _horizontalCastAngle,
125 | verticalCastAngle: _verticalCastAngle,
126 | shouldReactToIntefaceStyleChange: shouldReactToIntefaceStyleChange,
127 | shouldUseOverlayShadow: shouldUseOverlayShadow,
128 | shouldApplySurfaceBackground: shouldApplySurfaceBackground)
129 | }
130 | }
131 |
132 | public struct _ShadowPair {
133 | /// The overlay (highlight) shadow.
134 | public let top: CALayer._CanonicalShadowFormat?
135 | /// The main box shadow.
136 | public let bottom: CALayer._CanonicalShadowFormat
137 |
138 | public init(bottom: CALayer._CanonicalShadowFormat, top: CALayer._CanonicalShadowFormat? = nil) {
139 | self.bottom = bottom
140 | self.top = top
141 | }
142 | }
143 |
144 | public extension CALayer {
145 | /// Shadow format (as used in most design tools such as Sketch).
146 | struct _CanonicalShadowFormat {
147 | /// Global constant for no shadows.
148 | static let zero =
149 | _CanonicalShadowFormat(color: .clear, alpha: 0, offset: .zero, blur: 0, spread: 0)
150 |
151 | public var color: UIColor
152 | public var alpha: CGFloat
153 | public var offset: CGPoint
154 | public var blur: CGFloat
155 | public var spread: CGFloat
156 |
157 | /// Whether the shadow should be drawn or not.
158 | public var isVisible: Bool {
159 | self.alpha == 0
160 | }
161 |
162 | public init(
163 | color: UIColor,
164 | alpha: CGFloat = 0.16,
165 | offset: CGPoint,
166 | blur: CGFloat,
167 | spread: CGFloat = 0
168 | ) {
169 | self.color = color
170 | self.alpha = alpha
171 | self.offset = offset
172 | self.blur = blur
173 | self.spread = spread
174 | }
175 | }
176 | }
177 |
--------------------------------------------------------------------------------
/Sources/Surface/Surface.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 |
3 | open class SurfaceView: UIView {
4 | /// Returns its layer properly casted to `SurfaceLayer`.
5 | open var surfaceLayer: SurfaceLayer { layer as! SurfaceLayer }
6 |
7 | /// Returns the class used to create the layer for instances of this class.
8 | open override class var layerClass: AnyClass {
9 | SurfaceLayer.self
10 | }
11 | }
12 |
13 | public final class SurfaceLayer: CALayer {
14 | /// The shadow applied to the overlay layer.
15 | public var _appliedOverlayLayerShadow: CALayer._CanonicalShadowFormat {
16 | get { _overlayLayer._appliedLayerShadow }
17 | set { _overlayLayer._appliedLayerShadow = newValue }
18 | }
19 |
20 | /// The overlay layer adding a second shadow to it.
21 | private lazy var _overlayLayer: CALayer = {
22 | let layer = CALayer()
23 | addSublayer(layer)
24 | return layer
25 | }()
26 |
27 | /// The radius to use when drawing rounded corners for the layer’s background. Animatable.
28 | public override var cornerRadius: CGFloat {
29 | didSet {
30 | setNeedsLayout()
31 | setNeedsDisplay()
32 | }
33 | }
34 |
35 | /// Tells the layer to update its layout.
36 | public override func layoutSublayers() {
37 | _overlayLayer.frame = bounds
38 | _overlayLayer.backgroundColor = backgroundColor
39 | _overlayLayer.cornerRadius = cornerRadius
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/LinuxMain.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | import SurfaceTests
4 |
5 | var tests = [XCTestCaseEntry]()
6 | tests += SurfaceTests.allTests()
7 | XCTMain(tests)
8 |
--------------------------------------------------------------------------------
/Tests/SurfaceTests/SurfaceTests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 | @testable import Surface
3 |
4 | final class SurfaceTests: XCTestCase {
5 | func testExample() {
6 | }
7 |
8 | static var allTests = [
9 | ("testExample", testExample),
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/Tests/SurfaceTests/XCTestManifests.swift:
--------------------------------------------------------------------------------
1 | import XCTest
2 |
3 | #if !canImport(ObjectiveC)
4 | public func allTests() -> [XCTestCaseEntry] {
5 | return [
6 | testCase(SurfaceTests.allTests),
7 | ]
8 | }
9 | #endif
10 |
--------------------------------------------------------------------------------
/docs_/button.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdrone/Surface/a204af2e022b0e9c5df26d0940a226eb738d64f4/docs_/button.gif
--------------------------------------------------------------------------------
/docs_/switch.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/alexdrone/Surface/a204af2e022b0e9c5df26d0940a226eb738d64f4/docs_/switch.gif
--------------------------------------------------------------------------------