├── .gitignore ├── README.md └── RecreatingiMessageConfetti.playground ├── Pages ├── Step 1.xcplaygroundpage │ └── Contents.swift ├── Step 2.xcplaygroundpage │ └── Contents.swift ├── Step 3.xcplaygroundpage │ └── Contents.swift ├── Step 4.xcplaygroundpage │ └── Contents.swift ├── Step 5.xcplaygroundpage │ └── Contents.swift ├── Step 6.xcplaygroundpage │ └── Contents.swift ├── Step 7.xcplaygroundpage │ └── Contents.swift └── Step 8.xcplaygroundpage │ └── Contents.swift └── contents.xcplayground /.gitignore: -------------------------------------------------------------------------------- 1 | # Playgrounds 2 | timeline.xctimeline 3 | playground.xcworkspace 4 | 5 | # macOS 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Recreating iMessage Confetti 2 | 3 | ![Confetti Animation](http://bryce.co/recreating-imessage-confetti/confetti.gif) 4 | 5 | An Xcode Playground to acompany [this post](https://bryce.co/recreating-imessage-confetti/) on [bryce.co](https://bryce.co/). 6 | 7 | ## Usage 8 | 9 | Clone or download this repo and open `RecreatingiMessageConfetti.playground` in Xcode. 10 | 11 | This Playground is separated into different Pages to correlate with steps 12 | in the original post. 13 | 14 | To navigate between pages, you can either: 15 | 1) Use the Navigation Bar above the Editor 16 | 2) Use the Project Navigatior (`View > Navigators > Show Project Navigator`) 17 | 3) Use the links at the top & bottom of each page (`Editor > Show Rendered Markup`) 18 | 19 | Tested with Xcode 11.2.1 / Swift 5.1. 20 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 1.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | // Step 1: Creating Confetti Images 4 | 5 | /** 6 | Represents a single type of confetti piece. 7 | */ 8 | class ConfettiType { 9 | let color: UIColor 10 | let shape: ConfettiShape 11 | let position: ConfettiPosition 12 | 13 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 14 | self.color = color 15 | self.shape = shape 16 | self.position = position 17 | } 18 | 19 | lazy var image: UIImage = { 20 | let imageRect: CGRect = { 21 | switch shape { 22 | case .rectangle: 23 | return CGRect(x: 0, y: 0, width: 20, height: 13) 24 | case .circle: 25 | return CGRect(x: 0, y: 0, width: 10, height: 10) 26 | } 27 | }() 28 | 29 | UIGraphicsBeginImageContext(imageRect.size) 30 | let context = UIGraphicsGetCurrentContext()! 31 | context.setFillColor(color.cgColor) 32 | 33 | switch shape { 34 | case .rectangle: 35 | context.fill(imageRect) 36 | case .circle: 37 | context.fillEllipse(in: imageRect) 38 | } 39 | 40 | let image = UIGraphicsGetImageFromCurrentImageContext() 41 | UIGraphicsEndImageContext() 42 | return image! 43 | }() 44 | } 45 | 46 | enum ConfettiShape { 47 | case rectangle 48 | case circle 49 | } 50 | 51 | enum ConfettiPosition { 52 | case foreground 53 | case background 54 | } 55 | 56 | var confettiTypes: [ConfettiType] = { 57 | let confettiColors = [ 58 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 59 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 60 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 61 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 62 | 63 | // For each position x shape x color, construct an image 64 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 65 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 66 | return confettiColors.map { color in 67 | return ConfettiType(color: color, shape: shape, position: position) 68 | } 69 | } 70 | } 71 | }() 72 | 73 | //: [Next](@next) 74 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 2.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var image: UIImage = { 28 | let imageRect: CGRect = { 29 | switch shape { 30 | case .rectangle: 31 | return CGRect(x: 0, y: 0, width: 20, height: 13) 32 | case .circle: 33 | return CGRect(x: 0, y: 0, width: 10, height: 10) 34 | } 35 | }() 36 | 37 | UIGraphicsBeginImageContext(imageRect.size) 38 | let context = UIGraphicsGetCurrentContext()! 39 | context.setFillColor(color.cgColor) 40 | 41 | switch shape { 42 | case .rectangle: 43 | context.fill(imageRect) 44 | case .circle: 45 | context.fillEllipse(in: imageRect) 46 | } 47 | 48 | let image = UIGraphicsGetImageFromCurrentImageContext() 49 | UIGraphicsEndImageContext() 50 | return image! 51 | }() 52 | } 53 | 54 | enum ConfettiShape { 55 | case rectangle 56 | case circle 57 | } 58 | 59 | enum ConfettiPosition { 60 | case foreground 61 | case background 62 | } 63 | 64 | var confettiTypes: [ConfettiType] = { 65 | let confettiColors = [ 66 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 67 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 68 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 69 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 70 | 71 | // For each position x shape x color, construct an image 72 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 73 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 74 | return confettiColors.map { color in 75 | return ConfettiType(color: color, shape: shape, position: position) 76 | } 77 | } 78 | } 79 | }() 80 | 81 | // Step 2: Basic Emitter Layer Setup 82 | 83 | var confettiCells: [CAEmitterCell] = { 84 | return confettiTypes.map { confettiType in 85 | let cell = CAEmitterCell() 86 | 87 | cell.beginTime = 0.1 88 | cell.birthRate = 10 89 | cell.contents = confettiType.image.cgImage 90 | cell.emissionRange = CGFloat(Double.pi) 91 | cell.lifetime = 10 92 | cell.spin = 4 93 | cell.spinRange = 8 94 | cell.velocityRange = 100 95 | cell.yAcceleration = 150 96 | 97 | return cell 98 | } 99 | }() 100 | 101 | var confettiLayer: CAEmitterLayer = { 102 | let emitterLayer = CAEmitterLayer() 103 | 104 | emitterLayer.emitterCells = confettiCells 105 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.minY - 500) 106 | emitterLayer.emitterSize = CGSize(width: view.bounds.size.width, height: 500) 107 | emitterLayer.emitterShape = .rectangle 108 | emitterLayer.frame = view.bounds 109 | 110 | emitterLayer.beginTime = CACurrentMediaTime() 111 | return emitterLayer 112 | }() 113 | 114 | // And finally... 115 | 116 | view.layer.addSublayer(confettiLayer) 117 | PlaygroundPage.current.liveView = view 118 | 119 | //: [Next](@next) 120 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 3.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var image: UIImage = { 28 | let imageRect: CGRect = { 29 | switch shape { 30 | case .rectangle: 31 | return CGRect(x: 0, y: 0, width: 20, height: 13) 32 | case .circle: 33 | return CGRect(x: 0, y: 0, width: 10, height: 10) 34 | } 35 | }() 36 | 37 | UIGraphicsBeginImageContext(imageRect.size) 38 | let context = UIGraphicsGetCurrentContext()! 39 | context.setFillColor(color.cgColor) 40 | 41 | switch shape { 42 | case .rectangle: 43 | context.fill(imageRect) 44 | case .circle: 45 | context.fillEllipse(in: imageRect) 46 | } 47 | 48 | let image = UIGraphicsGetImageFromCurrentImageContext() 49 | UIGraphicsEndImageContext() 50 | return image! 51 | }() 52 | } 53 | 54 | enum ConfettiShape { 55 | case rectangle 56 | case circle 57 | } 58 | 59 | enum ConfettiPosition { 60 | case foreground 61 | case background 62 | } 63 | 64 | var confettiTypes: [ConfettiType] = { 65 | let confettiColors = [ 66 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 67 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 68 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 69 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 70 | 71 | // For each position x shape x color, construct an image 72 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 73 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 74 | return confettiColors.map { color in 75 | return ConfettiType(color: color, shape: shape, position: position) 76 | } 77 | } 78 | } 79 | }() 80 | 81 | // Step 2: Basic Emitter Layer Setup 82 | 83 | var confettiCells: [CAEmitterCell] = { 84 | return confettiTypes.map { confettiType in 85 | let cell = CAEmitterCell() 86 | 87 | cell.beginTime = 0.1 88 | cell.birthRate = 10 89 | cell.contents = confettiType.image.cgImage 90 | cell.emissionRange = CGFloat(Double.pi) 91 | cell.lifetime = 10 92 | cell.spin = 4 93 | cell.spinRange = 8 94 | cell.velocityRange = 100 95 | cell.yAcceleration = 150 96 | 97 | // Step 3: A _New_ Spin On Things 98 | 99 | cell.setValue("plane", forKey: "particleType") 100 | cell.setValue(Double.pi, forKey: "orientationRange") 101 | cell.setValue(Double.pi / 2, forKey: "orientationLongitude") 102 | cell.setValue(Double.pi / 2, forKey: "orientationLatitude") 103 | 104 | return cell 105 | } 106 | }() 107 | 108 | var confettiLayer: CAEmitterLayer = { 109 | let emitterLayer = CAEmitterLayer() 110 | 111 | emitterLayer.emitterCells = confettiCells 112 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.minY - 500) 113 | emitterLayer.emitterSize = CGSize(width: view.bounds.size.width, height: 500) 114 | emitterLayer.emitterShape = .rectangle 115 | emitterLayer.frame = view.bounds 116 | 117 | emitterLayer.beginTime = CACurrentMediaTime() 118 | return emitterLayer 119 | }() 120 | 121 | // And finally... 122 | 123 | view.layer.addSublayer(confettiLayer) 124 | PlaygroundPage.current.liveView = view 125 | 126 | //: [Next](@next) 127 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 4.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var image: UIImage = { 28 | let imageRect: CGRect = { 29 | switch shape { 30 | case .rectangle: 31 | return CGRect(x: 0, y: 0, width: 20, height: 13) 32 | case .circle: 33 | return CGRect(x: 0, y: 0, width: 10, height: 10) 34 | } 35 | }() 36 | 37 | UIGraphicsBeginImageContext(imageRect.size) 38 | let context = UIGraphicsGetCurrentContext()! 39 | context.setFillColor(color.cgColor) 40 | 41 | switch shape { 42 | case .rectangle: 43 | context.fill(imageRect) 44 | case .circle: 45 | context.fillEllipse(in: imageRect) 46 | } 47 | 48 | let image = UIGraphicsGetImageFromCurrentImageContext() 49 | UIGraphicsEndImageContext() 50 | return image! 51 | }() 52 | } 53 | 54 | enum ConfettiShape { 55 | case rectangle 56 | case circle 57 | } 58 | 59 | enum ConfettiPosition { 60 | case foreground 61 | case background 62 | } 63 | 64 | var confettiTypes: [ConfettiType] = { 65 | let confettiColors = [ 66 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 67 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 68 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 69 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 70 | 71 | // For each position x shape x color, construct an image 72 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 73 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 74 | return confettiColors.map { color in 75 | return ConfettiType(color: color, shape: shape, position: position) 76 | } 77 | } 78 | } 79 | }() 80 | 81 | // Step 2: Basic Emitter Layer Setup 82 | 83 | var confettiCells: [CAEmitterCell] = { 84 | return confettiTypes.map { confettiType in 85 | let cell = CAEmitterCell() 86 | 87 | cell.beginTime = 0.1 88 | cell.birthRate = 10 89 | cell.contents = confettiType.image.cgImage 90 | cell.emissionRange = CGFloat(Double.pi) 91 | cell.lifetime = 10 92 | cell.spin = 4 93 | cell.spinRange = 8 94 | cell.velocityRange = 100 95 | cell.yAcceleration = 150 96 | 97 | // Step 3: A _New_ Spin On Things 98 | 99 | cell.setValue("plane", forKey: "particleType") 100 | cell.setValue(Double.pi, forKey: "orientationRange") 101 | cell.setValue(Double.pi / 2, forKey: "orientationLongitude") 102 | cell.setValue(Double.pi / 2, forKey: "orientationLatitude") 103 | 104 | return cell 105 | } 106 | }() 107 | 108 | var confettiLayer: CAEmitterLayer = { 109 | let emitterLayer = CAEmitterLayer() 110 | 111 | emitterLayer.emitterCells = confettiCells 112 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.minY - 500) 113 | emitterLayer.emitterSize = CGSize(width: view.bounds.size.width, height: 500) 114 | emitterLayer.emitterShape = .rectangle 115 | emitterLayer.frame = view.bounds 116 | 117 | emitterLayer.beginTime = CACurrentMediaTime() 118 | return emitterLayer 119 | }() 120 | 121 | // Step 4: _Wave_ Hello to CAEmitterBehavior 122 | 123 | /* 124 | Returns a new CAEmitterBehavior with the given name. 125 | 126 | For Swift Playgrounds, it's easier to use runtime methods 127 | than to add a CAEmitterBehavior header to the project. 128 | Originally from https://bryce.co/caemitterbehavior/ 129 | */ 130 | func createBehavior(type: String) -> NSObject { 131 | let behaviorClass = NSClassFromString("CAEmitterBehavior") as! NSObject.Type 132 | let behaviorWithType = behaviorClass.method(for: NSSelectorFromString("behaviorWithType:"))! 133 | let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) 134 | return castedBehaviorWithType(behaviorClass, NSSelectorFromString("behaviorWithType:"), type) 135 | } 136 | 137 | func horizontalWaveBehavior() -> Any { 138 | let behavior = createBehavior(type: "wave") 139 | behavior.setValue([100, 0, 0], forKeyPath: "force") 140 | behavior.setValue(0.5, forKeyPath: "frequency") 141 | return behavior 142 | } 143 | 144 | func verticalWaveBehavior() -> Any { 145 | let behavior = createBehavior(type: "wave") 146 | behavior.setValue([0, 500, 0], forKeyPath: "force") 147 | behavior.setValue(3, forKeyPath: "frequency") 148 | return behavior 149 | } 150 | 151 | func addBehaviors() { 152 | confettiLayer.setValue([ 153 | horizontalWaveBehavior(), 154 | verticalWaveBehavior() 155 | ], forKey: "emitterBehaviors") 156 | } 157 | 158 | // And finally... 159 | 160 | view.layer.addSublayer(confettiLayer) 161 | addBehaviors() 162 | PlaygroundPage.current.liveView = view 163 | 164 | //: [Next](@next) 165 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 5.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var image: UIImage = { 28 | let imageRect: CGRect = { 29 | switch shape { 30 | case .rectangle: 31 | return CGRect(x: 0, y: 0, width: 20, height: 13) 32 | case .circle: 33 | return CGRect(x: 0, y: 0, width: 10, height: 10) 34 | } 35 | }() 36 | 37 | UIGraphicsBeginImageContext(imageRect.size) 38 | let context = UIGraphicsGetCurrentContext()! 39 | context.setFillColor(color.cgColor) 40 | 41 | switch shape { 42 | case .rectangle: 43 | context.fill(imageRect) 44 | case .circle: 45 | context.fillEllipse(in: imageRect) 46 | } 47 | 48 | let image = UIGraphicsGetImageFromCurrentImageContext() 49 | UIGraphicsEndImageContext() 50 | return image! 51 | }() 52 | } 53 | 54 | enum ConfettiShape { 55 | case rectangle 56 | case circle 57 | } 58 | 59 | enum ConfettiPosition { 60 | case foreground 61 | case background 62 | } 63 | 64 | var confettiTypes: [ConfettiType] = { 65 | let confettiColors = [ 66 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 67 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 68 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 69 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 70 | 71 | // For each position x shape x color, construct an image 72 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 73 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 74 | return confettiColors.map { color in 75 | return ConfettiType(color: color, shape: shape, position: position) 76 | } 77 | } 78 | } 79 | }() 80 | 81 | // Step 2: Basic Emitter Layer Setup 82 | 83 | var confettiCells: [CAEmitterCell] = { 84 | return confettiTypes.map { confettiType in 85 | let cell = CAEmitterCell() 86 | 87 | cell.beginTime = 0.1 88 | cell.birthRate = 10 89 | cell.contents = confettiType.image.cgImage 90 | cell.emissionRange = CGFloat(Double.pi) 91 | cell.lifetime = 10 92 | cell.spin = 4 93 | cell.spinRange = 8 94 | cell.velocityRange = 0 95 | cell.yAcceleration = 0 96 | 97 | // Step 3: A _New_ Spin On Things 98 | 99 | cell.setValue("plane", forKey: "particleType") 100 | cell.setValue(Double.pi, forKey: "orientationRange") 101 | cell.setValue(Double.pi / 2, forKey: "orientationLongitude") 102 | cell.setValue(Double.pi / 2, forKey: "orientationLatitude") 103 | 104 | return cell 105 | } 106 | }() 107 | 108 | var confettiLayer: CAEmitterLayer = { 109 | let emitterLayer = CAEmitterLayer() 110 | 111 | emitterLayer.emitterCells = confettiCells 112 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.midY) 113 | emitterLayer.emitterSize = CGSize(width: 100, height: 100) 114 | emitterLayer.emitterShape = .sphere 115 | emitterLayer.frame = view.bounds 116 | 117 | emitterLayer.beginTime = CACurrentMediaTime() 118 | return emitterLayer 119 | }() 120 | 121 | // Step 4: _Wave_ Hello to CAEmitterBehavior 122 | 123 | /* 124 | Returns a new CAEmitterBehavior with the given name. 125 | 126 | For Swift Playgrounds, it's easier to use runtime methods 127 | than to add a CAEmitterBehavior header to the project. 128 | Originally from https://bryce.co/caemitterbehavior/ 129 | */ 130 | func createBehavior(type: String) -> NSObject { 131 | let behaviorClass = NSClassFromString("CAEmitterBehavior") as! NSObject.Type 132 | let behaviorWithType = behaviorClass.method(for: NSSelectorFromString("behaviorWithType:"))! 133 | let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) 134 | return castedBehaviorWithType(behaviorClass, NSSelectorFromString("behaviorWithType:"), type) 135 | } 136 | 137 | func horizontalWaveBehavior() -> Any { 138 | let behavior = createBehavior(type: "wave") 139 | behavior.setValue([100, 0, 0], forKeyPath: "force") 140 | behavior.setValue(0.5, forKeyPath: "frequency") 141 | return behavior 142 | } 143 | 144 | func verticalWaveBehavior() -> Any { 145 | let behavior = createBehavior(type: "wave") 146 | behavior.setValue([0, 500, 0], forKeyPath: "force") 147 | behavior.setValue(3, forKeyPath: "frequency") 148 | return behavior 149 | } 150 | 151 | // Step 5: More _Attractive_ Confetti 152 | 153 | func attractorBehavior(for emitterLayer: CAEmitterLayer) -> Any { 154 | let behavior = createBehavior(type: "attractor") 155 | 156 | // Attractiveness 157 | behavior.setValue(-290, forKeyPath: "falloff") 158 | behavior.setValue(300, forKeyPath: "radius") 159 | behavior.setValue(10, forKeyPath: "stiffness") 160 | 161 | // Position 162 | behavior.setValue(CGPoint(x: emitterLayer.emitterPosition.x, 163 | y: emitterLayer.emitterPosition.y + 20), 164 | forKeyPath: "position") 165 | behavior.setValue(-70, forKeyPath: "zPosition") 166 | 167 | return behavior 168 | } 169 | 170 | func addBehaviors() { 171 | confettiLayer.setValue([ 172 | horizontalWaveBehavior(), 173 | verticalWaveBehavior(), 174 | attractorBehavior(for: confettiLayer) 175 | ], forKey: "emitterBehaviors") 176 | } 177 | 178 | // And finally... 179 | 180 | view.layer.addSublayer(confettiLayer) 181 | addBehaviors() 182 | PlaygroundPage.current.liveView = view 183 | 184 | //: [Next](@next) 185 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 6.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var image: UIImage = { 28 | let imageRect: CGRect = { 29 | switch shape { 30 | case .rectangle: 31 | return CGRect(x: 0, y: 0, width: 20, height: 13) 32 | case .circle: 33 | return CGRect(x: 0, y: 0, width: 10, height: 10) 34 | } 35 | }() 36 | 37 | UIGraphicsBeginImageContext(imageRect.size) 38 | let context = UIGraphicsGetCurrentContext()! 39 | context.setFillColor(color.cgColor) 40 | 41 | switch shape { 42 | case .rectangle: 43 | context.fill(imageRect) 44 | case .circle: 45 | context.fillEllipse(in: imageRect) 46 | } 47 | 48 | let image = UIGraphicsGetImageFromCurrentImageContext() 49 | UIGraphicsEndImageContext() 50 | return image! 51 | }() 52 | } 53 | 54 | enum ConfettiShape { 55 | case rectangle 56 | case circle 57 | } 58 | 59 | enum ConfettiPosition { 60 | case foreground 61 | case background 62 | } 63 | 64 | var confettiTypes: [ConfettiType] = { 65 | let confettiColors = [ 66 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 67 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 68 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 69 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 70 | 71 | // For each position x shape x color, construct an image 72 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 73 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 74 | return confettiColors.map { color in 75 | return ConfettiType(color: color, shape: shape, position: position) 76 | } 77 | } 78 | } 79 | }() 80 | 81 | // Step 2: Basic Emitter Layer Setup 82 | 83 | var confettiCells: [CAEmitterCell] = { 84 | return confettiTypes.map { confettiType in 85 | let cell = CAEmitterCell() 86 | 87 | cell.beginTime = 0.1 88 | cell.birthRate = 100 89 | cell.contents = confettiType.image.cgImage 90 | cell.emissionRange = CGFloat(Double.pi) 91 | cell.lifetime = 10 92 | cell.spin = 4 93 | cell.spinRange = 8 94 | cell.velocityRange = 0 95 | cell.yAcceleration = 0 96 | 97 | // Step 3: A _New_ Spin On Things 98 | 99 | cell.setValue("plane", forKey: "particleType") 100 | cell.setValue(Double.pi, forKey: "orientationRange") 101 | cell.setValue(Double.pi / 2, forKey: "orientationLongitude") 102 | cell.setValue(Double.pi / 2, forKey: "orientationLatitude") 103 | 104 | return cell 105 | } 106 | }() 107 | 108 | var confettiLayer: CAEmitterLayer = { 109 | let emitterLayer = CAEmitterLayer() 110 | 111 | emitterLayer.birthRate = 0 112 | emitterLayer.emitterCells = confettiCells 113 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.midY) 114 | emitterLayer.emitterSize = CGSize(width: 100, height: 100) 115 | emitterLayer.emitterShape = .sphere 116 | emitterLayer.frame = view.bounds 117 | 118 | emitterLayer.beginTime = CACurrentMediaTime() 119 | return emitterLayer 120 | }() 121 | 122 | // Step 4: _Wave_ Hello to CAEmitterBehavior 123 | 124 | /* 125 | Returns a new CAEmitterBehavior with the given name. 126 | 127 | For Swift Playgrounds, it's easier to use runtime methods 128 | than to add a CAEmitterBehavior header to the project. 129 | Originally from https://bryce.co/caemitterbehavior/ 130 | */ 131 | func createBehavior(type: String) -> NSObject { 132 | let behaviorClass = NSClassFromString("CAEmitterBehavior") as! NSObject.Type 133 | let behaviorWithType = behaviorClass.method(for: NSSelectorFromString("behaviorWithType:"))! 134 | let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) 135 | return castedBehaviorWithType(behaviorClass, NSSelectorFromString("behaviorWithType:"), type) 136 | } 137 | 138 | func horizontalWaveBehavior() -> Any { 139 | let behavior = createBehavior(type: "wave") 140 | behavior.setValue([100, 0, 0], forKeyPath: "force") 141 | behavior.setValue(0.5, forKeyPath: "frequency") 142 | return behavior 143 | } 144 | 145 | func verticalWaveBehavior() -> Any { 146 | let behavior = createBehavior(type: "wave") 147 | behavior.setValue([0, 500, 0], forKeyPath: "force") 148 | behavior.setValue(3, forKeyPath: "frequency") 149 | return behavior 150 | } 151 | 152 | // Step 5: More _Attractive_ Confetti 153 | 154 | func attractorBehavior(for emitterLayer: CAEmitterLayer) -> Any { 155 | let behavior = createBehavior(type: "attractor") 156 | behavior.setValue("attractor", forKeyPath: "name") 157 | 158 | // Attractiveness 159 | behavior.setValue(-290, forKeyPath: "falloff") 160 | behavior.setValue(300, forKeyPath: "radius") 161 | behavior.setValue(10, forKeyPath: "stiffness") 162 | 163 | // Position 164 | behavior.setValue(CGPoint(x: emitterLayer.emitterPosition.x, 165 | y: emitterLayer.emitterPosition.y + 20), 166 | forKeyPath: "position") 167 | behavior.setValue(-70, forKeyPath: "zPosition") 168 | 169 | return behavior 170 | } 171 | 172 | func addBehaviors() { 173 | confettiLayer.setValue([ 174 | horizontalWaveBehavior(), 175 | verticalWaveBehavior(), 176 | attractorBehavior(for: confettiLayer) 177 | ], forKey: "emitterBehaviors") 178 | } 179 | 180 | // Step 6: Animations & Explosions 181 | 182 | func addAttractorAnimation(to layer: CALayer) { 183 | let animation = CAKeyframeAnimation() 184 | animation.timingFunction = CAMediaTimingFunction(name: .easeOut) 185 | animation.duration = 3 186 | animation.keyTimes = [0, 0.4] 187 | animation.values = [80, 5] 188 | 189 | layer.add(animation, forKey: "emitterBehaviors.attractor.stiffness") 190 | } 191 | 192 | func addBirthrateAnimation(to layer: CALayer) { 193 | let animation = CABasicAnimation() 194 | animation.duration = 1 195 | animation.fromValue = 1 196 | animation.toValue = 0 197 | 198 | layer.add(animation, forKey: "birthRate") 199 | } 200 | 201 | func addAnimations() { 202 | addAttractorAnimation(to: confettiLayer) 203 | addBirthrateAnimation(to: confettiLayer) 204 | } 205 | 206 | // And finally... 207 | 208 | view.layer.addSublayer(confettiLayer) 209 | addBehaviors() 210 | addAnimations() 211 | PlaygroundPage.current.liveView = view 212 | 213 | //: [Next](@next) 214 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 7.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var name = UUID().uuidString 28 | 29 | lazy var image: UIImage = { 30 | let imageRect: CGRect = { 31 | switch shape { 32 | case .rectangle: 33 | return CGRect(x: 0, y: 0, width: 20, height: 13) 34 | case .circle: 35 | return CGRect(x: 0, y: 0, width: 10, height: 10) 36 | } 37 | }() 38 | 39 | UIGraphicsBeginImageContext(imageRect.size) 40 | let context = UIGraphicsGetCurrentContext()! 41 | context.setFillColor(color.cgColor) 42 | 43 | switch shape { 44 | case .rectangle: 45 | context.fill(imageRect) 46 | case .circle: 47 | context.fillEllipse(in: imageRect) 48 | } 49 | 50 | let image = UIGraphicsGetImageFromCurrentImageContext() 51 | UIGraphicsEndImageContext() 52 | return image! 53 | }() 54 | } 55 | 56 | enum ConfettiShape { 57 | case rectangle 58 | case circle 59 | } 60 | 61 | enum ConfettiPosition { 62 | case foreground 63 | case background 64 | } 65 | 66 | var confettiTypes: [ConfettiType] = { 67 | let confettiColors = [ 68 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 69 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 70 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 71 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 72 | 73 | // For each position x shape x color, construct an image 74 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 75 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 76 | return confettiColors.map { color in 77 | return ConfettiType(color: color, shape: shape, position: position) 78 | } 79 | } 80 | } 81 | }() 82 | 83 | // Step 2: Basic Emitter Layer Setup 84 | 85 | var confettiCells: [CAEmitterCell] = { 86 | return confettiTypes.map { confettiType in 87 | let cell = CAEmitterCell() 88 | cell.name = confettiType.name 89 | 90 | cell.beginTime = 0.1 91 | cell.birthRate = 100 92 | cell.contents = confettiType.image.cgImage 93 | cell.emissionRange = CGFloat(Double.pi) 94 | cell.lifetime = 10 95 | cell.spin = 4 96 | cell.spinRange = 8 97 | cell.velocityRange = 0 98 | cell.yAcceleration = 0 99 | 100 | // Step 3: A _New_ Spin On Things 101 | 102 | cell.setValue("plane", forKey: "particleType") 103 | cell.setValue(Double.pi, forKey: "orientationRange") 104 | cell.setValue(Double.pi / 2, forKey: "orientationLongitude") 105 | cell.setValue(Double.pi / 2, forKey: "orientationLatitude") 106 | 107 | return cell 108 | } 109 | }() 110 | 111 | var confettiLayer: CAEmitterLayer = { 112 | let emitterLayer = CAEmitterLayer() 113 | 114 | emitterLayer.birthRate = 0 115 | emitterLayer.emitterCells = confettiCells 116 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.minY - 100) 117 | emitterLayer.emitterSize = CGSize(width: 100, height: 100) 118 | emitterLayer.emitterShape = .sphere 119 | emitterLayer.frame = view.bounds 120 | 121 | emitterLayer.beginTime = CACurrentMediaTime() 122 | return emitterLayer 123 | }() 124 | 125 | // Step 4: _Wave_ Hello to CAEmitterBehavior 126 | 127 | /* 128 | Returns a new CAEmitterBehavior with the given name. 129 | 130 | For Swift Playgrounds, it's easier to use runtime methods 131 | than to add a CAEmitterBehavior header to the project. 132 | Originally from https://bryce.co/caemitterbehavior/ 133 | */ 134 | func createBehavior(type: String) -> NSObject { 135 | let behaviorClass = NSClassFromString("CAEmitterBehavior") as! NSObject.Type 136 | let behaviorWithType = behaviorClass.method(for: NSSelectorFromString("behaviorWithType:"))! 137 | let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) 138 | return castedBehaviorWithType(behaviorClass, NSSelectorFromString("behaviorWithType:"), type) 139 | } 140 | 141 | func horizontalWaveBehavior() -> Any { 142 | let behavior = createBehavior(type: "wave") 143 | behavior.setValue([100, 0, 0], forKeyPath: "force") 144 | behavior.setValue(0.5, forKeyPath: "frequency") 145 | return behavior 146 | } 147 | 148 | func verticalWaveBehavior() -> Any { 149 | let behavior = createBehavior(type: "wave") 150 | behavior.setValue([0, 500, 0], forKeyPath: "force") 151 | behavior.setValue(3, forKeyPath: "frequency") 152 | return behavior 153 | } 154 | 155 | // Step 5: More _Attractive_ Confetti 156 | 157 | func attractorBehavior(for emitterLayer: CAEmitterLayer) -> Any { 158 | let behavior = createBehavior(type: "attractor") 159 | behavior.setValue("attractor", forKeyPath: "name") 160 | 161 | // Attractiveness 162 | behavior.setValue(-290, forKeyPath: "falloff") 163 | behavior.setValue(300, forKeyPath: "radius") 164 | behavior.setValue(10, forKeyPath: "stiffness") 165 | 166 | // Position 167 | behavior.setValue(CGPoint(x: emitterLayer.emitterPosition.x, 168 | y: emitterLayer.emitterPosition.y + 20), 169 | forKeyPath: "position") 170 | behavior.setValue(-70, forKeyPath: "zPosition") 171 | 172 | return behavior 173 | } 174 | 175 | func addBehaviors() { 176 | confettiLayer.setValue([ 177 | horizontalWaveBehavior(), 178 | verticalWaveBehavior(), 179 | attractorBehavior(for: confettiLayer) 180 | ], forKey: "emitterBehaviors") 181 | } 182 | 183 | // Step 6: Animations & Explosions 184 | 185 | func addAttractorAnimation(to layer: CALayer) { 186 | let animation = CAKeyframeAnimation() 187 | animation.timingFunction = CAMediaTimingFunction(name: .easeOut) 188 | animation.duration = 3 189 | animation.keyTimes = [0, 0.4] 190 | animation.values = [80, 5] 191 | 192 | layer.add(animation, forKey: "emitterBehaviors.attractor.stiffness") 193 | } 194 | 195 | func addBirthrateAnimation(to layer: CALayer) { 196 | let animation = CABasicAnimation() 197 | animation.duration = 1 198 | animation.fromValue = 1 199 | animation.toValue = 0 200 | 201 | layer.add(animation, forKey: "birthRate") 202 | } 203 | 204 | func addAnimations() { 205 | addAttractorAnimation(to: confettiLayer) 206 | addBirthrateAnimation(to: confettiLayer) 207 | addGravityAnimation(to: confettiLayer) 208 | } 209 | 210 | // Step 7: Air Resistance & Gravity 211 | 212 | func dragBehavior() -> Any { 213 | let behavior = createBehavior(type: "drag") 214 | behavior.setValue("drag", forKey: "name") 215 | behavior.setValue(2, forKey: "drag") 216 | 217 | return behavior 218 | } 219 | 220 | func addDragAnimation(to layer: CALayer) { 221 | let animation = CABasicAnimation() 222 | animation.duration = 0.35 223 | animation.fromValue = 0 224 | animation.toValue = 2 225 | 226 | layer.add(animation, forKey: "emitterBehaviors.drag.drag") 227 | } 228 | 229 | func addGravityAnimation(to layer: CALayer) { 230 | let animation = CAKeyframeAnimation() 231 | animation.duration = 6 232 | animation.keyTimes = [0.05, 0.1, 0.5, 1] 233 | animation.values = [0, 100, 2000, 4000] 234 | 235 | for image in confettiTypes { 236 | layer.add(animation, forKey: "emitterCells.\(image.name).yAcceleration") 237 | } 238 | } 239 | 240 | // And finally... 241 | 242 | view.layer.addSublayer(confettiLayer) 243 | addBehaviors() 244 | addAnimations() 245 | PlaygroundPage.current.liveView = view 246 | 247 | //: [Next](@next) 248 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/Pages/Step 8.xcplaygroundpage/Contents.swift: -------------------------------------------------------------------------------- 1 | //: [Previous](@previous) 2 | 3 | import PlaygroundSupport 4 | import UIKit 5 | 6 | // Step 0: Playground Setup 7 | let view = UIView() 8 | view.backgroundColor = .white 9 | view.frame = CGRect(x: 0, y: 0, width: 500, height: 500) 10 | 11 | // Step 1: Creating Confetti Images 12 | 13 | /** 14 | Represents a single type of confetti piece. 15 | */ 16 | class ConfettiType { 17 | let color: UIColor 18 | let shape: ConfettiShape 19 | let position: ConfettiPosition 20 | 21 | init(color: UIColor, shape: ConfettiShape, position: ConfettiPosition) { 22 | self.color = color 23 | self.shape = shape 24 | self.position = position 25 | } 26 | 27 | lazy var name = UUID().uuidString 28 | 29 | lazy var image: UIImage = { 30 | let imageRect: CGRect = { 31 | switch shape { 32 | case .rectangle: 33 | return CGRect(x: 0, y: 0, width: 20, height: 13) 34 | case .circle: 35 | return CGRect(x: 0, y: 0, width: 10, height: 10) 36 | } 37 | }() 38 | 39 | UIGraphicsBeginImageContext(imageRect.size) 40 | let context = UIGraphicsGetCurrentContext()! 41 | context.setFillColor(color.cgColor) 42 | 43 | switch shape { 44 | case .rectangle: 45 | context.fill(imageRect) 46 | case .circle: 47 | context.fillEllipse(in: imageRect) 48 | } 49 | 50 | let image = UIGraphicsGetImageFromCurrentImageContext() 51 | UIGraphicsEndImageContext() 52 | return image! 53 | }() 54 | } 55 | 56 | enum ConfettiShape { 57 | case rectangle 58 | case circle 59 | } 60 | 61 | enum ConfettiPosition { 62 | case foreground 63 | case background 64 | } 65 | 66 | var confettiTypes: [ConfettiType] = { 67 | let confettiColors = [ 68 | (r:149,g:58,b:255), (r:255,g:195,b:41), (r:255,g:101,b:26), 69 | (r:123,g:92,b:255), (r:76,g:126,b:255), (r:71,g:192,b:255), 70 | (r:255,g:47,b:39), (r:255,g:91,b:134), (r:233,g:122,b:208) 71 | ].map { UIColor(red: $0.r / 255.0, green: $0.g / 255.0, blue: $0.b / 255.0, alpha: 1) } 72 | 73 | // For each position x shape x color, construct an image 74 | return [ConfettiPosition.foreground, ConfettiPosition.background].flatMap { position in 75 | return [ConfettiShape.rectangle, ConfettiShape.circle].flatMap { shape in 76 | return confettiColors.map { color in 77 | return ConfettiType(color: color, shape: shape, position: position) 78 | } 79 | } 80 | } 81 | }() 82 | 83 | // Step 2: Basic Emitter Layer Setup 84 | 85 | func createConfettiCells() -> [CAEmitterCell] { 86 | return confettiTypes.map { confettiType in 87 | let cell = CAEmitterCell() 88 | cell.name = confettiType.name 89 | 90 | cell.beginTime = 0.1 91 | cell.birthRate = 100 92 | cell.contents = confettiType.image.cgImage 93 | cell.emissionRange = CGFloat(Double.pi) 94 | cell.lifetime = 10 95 | cell.spin = 4 96 | cell.spinRange = 8 97 | cell.velocityRange = 0 98 | cell.yAcceleration = 0 99 | 100 | // Step 3: A _New_ Spin On Things 101 | 102 | cell.setValue("plane", forKey: "particleType") 103 | cell.setValue(Double.pi, forKey: "orientationRange") 104 | cell.setValue(Double.pi / 2, forKey: "orientationLongitude") 105 | cell.setValue(Double.pi / 2, forKey: "orientationLatitude") 106 | 107 | return cell 108 | } 109 | } 110 | 111 | // Step 4: _Wave_ Hello to CAEmitterBehavior 112 | 113 | /* 114 | Returns a new CAEmitterBehavior with the given name. 115 | 116 | For Swift Playgrounds, it's easier to use runtime methods 117 | than to add a CAEmitterBehavior header to the project. 118 | Originally from https://bryce.co/caemitterbehavior/ 119 | */ 120 | func createBehavior(type: String) -> NSObject { 121 | let behaviorClass = NSClassFromString("CAEmitterBehavior") as! NSObject.Type 122 | let behaviorWithType = behaviorClass.method(for: NSSelectorFromString("behaviorWithType:"))! 123 | let castedBehaviorWithType = unsafeBitCast(behaviorWithType, to:(@convention(c)(Any?, Selector, Any?) -> NSObject).self) 124 | return castedBehaviorWithType(behaviorClass, NSSelectorFromString("behaviorWithType:"), type) 125 | } 126 | 127 | func horizontalWaveBehavior() -> Any { 128 | let behavior = createBehavior(type: "wave") 129 | behavior.setValue([100, 0, 0], forKeyPath: "force") 130 | behavior.setValue(0.5, forKeyPath: "frequency") 131 | return behavior 132 | } 133 | 134 | func verticalWaveBehavior() -> Any { 135 | let behavior = createBehavior(type: "wave") 136 | behavior.setValue([0, 500, 0], forKeyPath: "force") 137 | behavior.setValue(3, forKeyPath: "frequency") 138 | return behavior 139 | } 140 | 141 | // Step 5: More _Attractive_ Confetti 142 | 143 | func attractorBehavior(for emitterLayer: CAEmitterLayer) -> Any { 144 | let behavior = createBehavior(type: "attractor") 145 | behavior.setValue("attractor", forKeyPath: "name") 146 | 147 | // Attractiveness 148 | behavior.setValue(-290, forKeyPath: "falloff") 149 | behavior.setValue(300, forKeyPath: "radius") 150 | behavior.setValue(10, forKeyPath: "stiffness") 151 | 152 | // Position 153 | behavior.setValue(CGPoint(x: emitterLayer.emitterPosition.x, 154 | y: emitterLayer.emitterPosition.y + 20), 155 | forKeyPath: "position") 156 | behavior.setValue(-70, forKeyPath: "zPosition") 157 | 158 | return behavior 159 | } 160 | 161 | func addBehaviors(to layer: CAEmitterLayer) { 162 | layer.setValue([ 163 | horizontalWaveBehavior(), 164 | verticalWaveBehavior(), 165 | attractorBehavior(for: layer) 166 | ], forKey: "emitterBehaviors") 167 | } 168 | 169 | // Step 6: Animations & Explosions 170 | 171 | func addAttractorAnimation(to layer: CALayer) { 172 | let animation = CAKeyframeAnimation() 173 | animation.timingFunction = CAMediaTimingFunction(name: .easeOut) 174 | animation.duration = 3 175 | animation.keyTimes = [0, 0.4] 176 | animation.values = [80, 5] 177 | 178 | layer.add(animation, forKey: "emitterBehaviors.attractor.stiffness") 179 | } 180 | 181 | func addBirthrateAnimation(to layer: CALayer) { 182 | let animation = CABasicAnimation() 183 | animation.duration = 1 184 | animation.fromValue = 1 185 | animation.toValue = 0 186 | 187 | layer.add(animation, forKey: "birthRate") 188 | } 189 | 190 | func addAnimations(to layer: CAEmitterLayer) { 191 | addAttractorAnimation(to: layer) 192 | addBirthrateAnimation(to: layer) 193 | addGravityAnimation(to: layer) 194 | } 195 | 196 | // Step 7: Air Resistance & Gravity 197 | 198 | func dragBehavior() -> Any { 199 | let behavior = createBehavior(type: "drag") 200 | behavior.setValue("drag", forKey: "name") 201 | behavior.setValue(2, forKey: "drag") 202 | 203 | return behavior 204 | } 205 | 206 | func addDragAnimation(to layer: CALayer) { 207 | let animation = CABasicAnimation() 208 | animation.duration = 0.35 209 | animation.fromValue = 0 210 | animation.toValue = 2 211 | 212 | layer.add(animation, forKey: "emitterBehaviors.drag.drag") 213 | } 214 | 215 | func addGravityAnimation(to layer: CALayer) { 216 | let animation = CAKeyframeAnimation() 217 | animation.duration = 6 218 | animation.keyTimes = [0.05, 0.1, 0.5, 1] 219 | animation.values = [0, 100, 2000, 4000] 220 | 221 | for image in confettiTypes { 222 | layer.add(animation, forKey: "emitterCells.\(image.name).yAcceleration") 223 | } 224 | } 225 | 226 | // Step 8: Background & Foreground 227 | 228 | func createConfettiLayer() -> CAEmitterLayer { 229 | let emitterLayer = CAEmitterLayer() 230 | 231 | emitterLayer.birthRate = 0 232 | emitterLayer.emitterCells = createConfettiCells() 233 | emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.minY - 100) 234 | emitterLayer.emitterSize = CGSize(width: 100, height: 100) 235 | emitterLayer.emitterShape = .sphere 236 | emitterLayer.frame = view.bounds 237 | 238 | emitterLayer.beginTime = CACurrentMediaTime() 239 | return emitterLayer 240 | } 241 | 242 | var foregroundConfettiLayer = createConfettiLayer() 243 | 244 | var backgroundConfettiLayer: CAEmitterLayer = { 245 | let emitterLayer = createConfettiLayer() 246 | 247 | for emitterCell in emitterLayer.emitterCells ?? [] { 248 | emitterCell.scale = 0.5 249 | } 250 | 251 | emitterLayer.opacity = 0.5 252 | emitterLayer.speed = 0.95 253 | 254 | return emitterLayer 255 | }() 256 | 257 | // And finally... 258 | 259 | for layer in [foregroundConfettiLayer, backgroundConfettiLayer] { 260 | view.layer.addSublayer(layer) 261 | addBehaviors(to: layer) 262 | addAnimations(to: layer) 263 | } 264 | 265 | PlaygroundPage.current.liveView = view 266 | -------------------------------------------------------------------------------- /RecreatingiMessageConfetti.playground/contents.xcplayground: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------