Skip to content

Commit

Permalink
Adds ConfettiMode
Browse files Browse the repository at this point in the history
  • Loading branch information
simonbs committed Jan 2, 2023
1 parent 8195419 commit 3c53234
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 27 deletions.
56 changes: 42 additions & 14 deletions Example/Example/ViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,64 @@ import UIKit
class ViewController: UIViewController {
private let emojiLabel: UILabel = {
let this = UILabel()
this.translatesAutoresizingMaskIntoConstraints = false
this.font = .systemFont(ofSize: 64)
this.text = "🎉"
return this
}()
private let confettiView: ConfettiView = {
let images = (1 ... 7).compactMap { UIImage(named: "confetti\($0)") }
let this = ConfettiView(images: images)
private var confettiView: ConfettiView?
private let segmentedControl: UISegmentedControl = {
let this = UISegmentedControl()
this.translatesAutoresizingMaskIntoConstraints = false
this.insertSegment(withTitle: "Top to Bottom", at: 0, animated: false)
this.insertSegment(withTitle: "Center to Left", at: 1, animated: false)
this.selectedSegmentIndex = 0
return this
}()

override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = .systemBackground
view.addSubview(emojiLabel)
view.addSubview(confettiView)
NSLayoutConstraint.activate([
emojiLabel.centerXAnchor.constraint(equalTo: view.centerXAnchor),
emojiLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
view.addSubview(segmentedControl)
segmentedControl.addTarget(self, action: #selector(segmentedControlValueChanged), for: .valueChanged)
setupConfettiView(with: .topToBottom)
}

confettiView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
confettiView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
confettiView.topAnchor.constraint(equalTo: view.topAnchor),
confettiView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
])
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let emojiLabelSize = emojiLabel.intrinsicContentSize
let emojiLabelOrigin = CGPoint(x: (view.frame.width - emojiLabelSize.width) / 2, y: (view.frame.height - emojiLabelSize.height) / 2)
emojiLabel.frame = CGRect(origin: emojiLabelOrigin, size: emojiLabelSize)
confettiView?.frame = view.bounds
let segmentedControlSize = segmentedControl.intrinsicContentSize
let segmentedControlOriginX = (view.frame.width - segmentedControlSize.width) / 2
let segmentedControlOriginY = view.frame.height - view.safeAreaInsets.bottom - segmentedControlSize.height - 30
let segmentedControlOrigin = CGPoint(x: segmentedControlOriginX, y: segmentedControlOriginY)
segmentedControl.frame = CGRect(origin: segmentedControlOrigin, size: segmentedControlSize)
}

override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
confettiView.shoot()
confettiView?.shoot()
}
}

private extension ViewController {
private func setupConfettiView(with mode: ConfettiMode) {
confettiView?.removeFromSuperview()
let images = (1 ... 7).compactMap { UIImage(named: "confetti\($0)") }
let confettiView = ConfettiView(mode: mode, images: images)
confettiView.isUserInteractionEnabled = false
view.addSubview(confettiView)
self.confettiView = confettiView
view.setNeedsLayout()
}

@objc private func segmentedControlValueChanged() {
if segmentedControl.selectedSegmentIndex == 0 {
setupConfettiView(with: .topToBottom)
} else if segmentedControl.selectedSegmentIndex == 1{
setupConfettiView(with: .centerToLeft)
}
}
}
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// swift-tools-version: 5.6
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "ConfettiKit",
platforms: [.iOS(.v15)],
platforms: [.iOS(.v15), .macCatalyst(.v15)],
products: [
.library(name: "ConfettiKit", targets: ["ConfettiKit"]),
],
Expand Down
45 changes: 39 additions & 6 deletions Sources/ConfettiKit/ConfettiLayerView.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import UIKit

final class ConfettiLayerView: UIView {
private let mode: ConfettiMode
private let emitterLayer: CAEmitterLayer = {
var behaviors: [NSObject] = []
if let behavior = EmitterBehavior.makeHorizontalWaveBehavior() {
Expand All @@ -10,7 +11,6 @@ final class ConfettiLayerView: UIView {
behaviors.append(behavior)
}
let this = CAEmitterLayer()
this.emitterShape = .line
this.birthRate = 0
this.lifetime = 0
if !behaviors.isEmpty {
Expand All @@ -25,13 +25,15 @@ final class ConfettiLayerView: UIView {
private let scaleRange: CGFloat
private let speed: Float

init(images: [UIImage], birthRate: Float = 100, scale: CGFloat = 1, scaleRange: CGFloat = 0, speed: Float = 1) {
init(mode: ConfettiMode, images: [UIImage], birthRate: Float = 100, scale: CGFloat = 1, scaleRange: CGFloat = 0, speed: Float = 1) {
self.mode = mode
self.images = images
self.birthRate = birthRate
self.scale = scale
self.scaleRange = scaleRange
self.speed = speed
super.init(frame: .zero)
emitterLayer.emitterShape = mode.emitterShape
layer.addSublayer(emitterLayer)
}

Expand All @@ -42,7 +44,7 @@ final class ConfettiLayerView: UIView {
override func layoutSubviews() {
super.layoutSubviews()
emitterLayer.emitterSize = CGSize(width: bounds.width, height: 0)
emitterLayer.emitterPosition = CGPoint(x: bounds.midX, y: -10)
emitterLayer.emitterPosition = mode.emitterPosition(in: bounds)
emitterLayer.frame = bounds
updateAttractorBehavior()
}
Expand Down Expand Up @@ -139,12 +141,23 @@ private extension ConfettiLayerView {
private func addGravityAnimation() {
let animation = CAKeyframeAnimation()
animation.duration = 6
animation.keyTimes = [0.05, 0.1, 0.5, 1]
animation.values = [0, 300, 750, 1000]
switch mode {
case .topToBottom:
animation.keyTimes = [0.05, 0.1, 0.5, 1]
animation.values = [0, 300, 750, 1000]
case .centerToLeft:
animation.keyTimes = [0, 0.1, 0.5, 1]
animation.values = [-1000, -750, -100, 0]
}
let cells = emitterLayer.emitterCells ?? []
for cell in cells {
if let name = cell.name {
emitterLayer.add(animation, forKey: "emitterCells.\(name).yAcceleration")
switch mode {
case .topToBottom:
emitterLayer.add(animation, forKey: "emitterCells.\(name).yAcceleration")
case .centerToLeft:
emitterLayer.add(animation, forKey: "emitterCells.\(name).xAcceleration")
}
}
}
}
Expand All @@ -155,3 +168,23 @@ private extension ConfettiLayerView {
return radians.value
}
}

private extension ConfettiMode {
var emitterShape: CAEmitterLayerEmitterShape {
switch self {
case .topToBottom:
return .line
case .centerToLeft:
return .point
}
}

func emitterPosition(in rect: CGRect) -> CGPoint {
switch self {
case .topToBottom:
return CGPoint(x: rect.midX, y: -10)
case .centerToLeft:
return CGPoint(x: rect.midX, y: rect.midY)
}
}
}
4 changes: 4 additions & 0 deletions Sources/ConfettiKit/ConfettiMode.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
public enum ConfettiMode {
case topToBottom
case centerToLeft
}
6 changes: 3 additions & 3 deletions Sources/ConfettiKit/ConfettiShotView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ final class ConfettiShotView: UIView {
private let backgroundLayerView: ConfettiLayerView
private let foregroundLayerView: ConfettiLayerView

init(images: [UIImage]) {
backgroundLayerView = ConfettiLayerView(images: images, birthRate: 20, scale: 0.6, speed: 0.95)
foregroundLayerView = ConfettiLayerView(images: images, birthRate: 48, scale: 0.8, scaleRange: 0.1)
init(mode: ConfettiMode, images: [UIImage], birthRate1: Float, birthRate2: Float) {
backgroundLayerView = ConfettiLayerView(mode: mode, images: images, birthRate: birthRate1, scale: 0.6, speed: 0.95)
foregroundLayerView = ConfettiLayerView(mode: mode, images: images, birthRate: birthRate2, scale: 0.8, scaleRange: 0.1)
backgroundLayerView.alpha = 0.5
super.init(frame: .zero)
addSubview(backgroundLayerView)
Expand Down
9 changes: 7 additions & 2 deletions Sources/ConfettiKit/ConfettiView.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import UIKit

public final class ConfettiView: UIView {
public var birthRate1: Float = 48
public var birthRate2: Float = 20

private let mode: ConfettiMode
private let images: [UIImage]
private var shootings: [Shooting] = []

public init(images: [UIImage]) {
public init(mode: ConfettiMode = .topToBottom, images: [UIImage]) {
self.mode = mode
self.images = images
super.init(frame: .zero)
clipsToBounds = true
Expand All @@ -22,7 +27,7 @@ public final class ConfettiView: UIView {
}

public func shoot() {
let view = ConfettiShotView(images: images)
let view = ConfettiShotView(mode: mode, images: images, birthRate1: birthRate1, birthRate2: birthRate2)
view.frame = bounds
addSubview(view)
let shooting = Shooting(view: view)
Expand Down

0 comments on commit 3c53234

Please sign in to comment.