From 3c532340639661384221e405afda896f2f1c4ff7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=B8vring?= Date: Tue, 3 Jan 2023 00:45:47 +0100 Subject: [PATCH] Adds ConfettiMode --- Example/Example/ViewController.swift | 56 +++++++++++++++------ Package.swift | 4 +- Sources/ConfettiKit/ConfettiLayerView.swift | 45 ++++++++++++++--- Sources/ConfettiKit/ConfettiMode.swift | 4 ++ Sources/ConfettiKit/ConfettiShotView.swift | 6 +-- Sources/ConfettiKit/ConfettiView.swift | 9 +++- 6 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 Sources/ConfettiKit/ConfettiMode.swift diff --git a/Example/Example/ViewController.swift b/Example/Example/ViewController.swift index 1ff9759..d8176e2 100644 --- a/Example/Example/ViewController.swift +++ b/Example/Example/ViewController.swift @@ -11,15 +11,17 @@ 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 }() @@ -27,20 +29,46 @@ class ViewController: UIViewController { 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, 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) + } } } diff --git a/Package.swift b/Package.swift index e638456..5c8dcdc 100644 --- a/Package.swift +++ b/Package.swift @@ -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"]), ], diff --git a/Sources/ConfettiKit/ConfettiLayerView.swift b/Sources/ConfettiKit/ConfettiLayerView.swift index a128ac7..96c8eb7 100644 --- a/Sources/ConfettiKit/ConfettiLayerView.swift +++ b/Sources/ConfettiKit/ConfettiLayerView.swift @@ -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() { @@ -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 { @@ -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) } @@ -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() } @@ -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") + } } } } @@ -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) + } + } +} diff --git a/Sources/ConfettiKit/ConfettiMode.swift b/Sources/ConfettiKit/ConfettiMode.swift new file mode 100644 index 0000000..c9ddd38 --- /dev/null +++ b/Sources/ConfettiKit/ConfettiMode.swift @@ -0,0 +1,4 @@ +public enum ConfettiMode { + case topToBottom + case centerToLeft +} diff --git a/Sources/ConfettiKit/ConfettiShotView.swift b/Sources/ConfettiKit/ConfettiShotView.swift index bd3553e..85a0710 100644 --- a/Sources/ConfettiKit/ConfettiShotView.swift +++ b/Sources/ConfettiKit/ConfettiShotView.swift @@ -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) diff --git a/Sources/ConfettiKit/ConfettiView.swift b/Sources/ConfettiKit/ConfettiView.swift index 604b683..b286187 100644 --- a/Sources/ConfettiKit/ConfettiView.swift +++ b/Sources/ConfettiKit/ConfettiView.swift @@ -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 @@ -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)