Skip to content

Instantly share code, notes, and snippets.

@harrisonweinerman
Last active September 15, 2017 03:16
Show Gist options
  • Save harrisonweinerman/791ee0d2b55f5ce09ce3001dd4149e54 to your computer and use it in GitHub Desktop.
Save harrisonweinerman/791ee0d2b55f5ce09ce3001dd4149e54 to your computer and use it in GitHub Desktop.
I wrote this library to create the raining Monkey emoji effect when you first download Monkey-- view it in action here! https://www.dropbox.com/s/ue90xef6dd9wb8w/monkeyconfetti.gif?dl=0 Copyright Monkey Squad Inc. and reproduced here with permission. Please attribute Monkey Squad Inc. if referencing.
//
// ConfettiView.swift
// Monkey
//
// Created by Harrison Weinerman on 3/25/17.
// Copyright © 2017 Monkey Squad, Inc. All rights reserved.
//
import UIKit
import CoreMotion
/**
View that rains Monkey emojis with gravity and collision behaviors and adjusts position based on how user tilts phone
*/
class ConfettiView: UIView {
/// Emojis generated by the sprinkler (items will have collision/gravity effects)
private var emojiImageViews = [UIImageView]()
/// Animator that manages gravity and collision behaviors for confetti particles.
private var animator : UIDynamicAnimator!
/// Gravity behavior that causes all monkeys to drop. Affected continuously by device motion.
private var gravityBehavior = UIGravityBehavior()
/// Collision behavior that causes monkeys to collide with each other and the edges of the view
private var collision = UICollisionBehavior()
/// Used to call sprinkle() repeatedly to drop many confetti pieces
private var sprinkleTimer : Timer!
/// Tracks how user moves their phone to adjust the gravity of confetti particles
private var motionManager = CMMotionManager()
/// Manages beginning animation and tearing down when animation should finish
/// When true, confetti pieces are currently raining from ConfettiView. When false, it has not started raining yet. This value is only false very briefly when ConfettiView is instantiated as it starts sprinkling in draw().
var isAnimating = false {
didSet {
guard oldValue != isAnimating else {
return // State not changed.
}
if isAnimating {
// Only start adding confetti timer once
// Every 0.13 seconds (determined by trial and error to achieve desired effect) we add and drop another emoji by calling sprinkle()
sprinkleTimer = Timer.scheduledTimer(timeInterval: 0.13,
target: self,
selector: #selector(sprinkle),
userInfo: nil,
repeats: true)
} else {
// Tear down
sprinkleTimer.invalidate()
self.emojiImageViews.forEach { $0.removeFromSuperview() }
}
}
}
override init(frame: CGRect) {
super.init(frame: frame)
self.afterInit()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
self.afterInit()
}
// Sets up behavious and animations.
private func afterInit() {
// Generate the animator the gravity behavior.
gravityBehavior.action = { () -> (Void) in
self.emojiImageViews.forEach {
// Remove items that fell off the screen.
if $0.frame.origin.y > self.frame.size.height + 50 {
$0.removeFromSuperview()
}
}
}
// Listen for updates.
motionManager.startDeviceMotionUpdates(to: OperationQueue.main) { (motion, error) in
// Set gravity to acceleration from CMAcceleration.
let gravityAccel = motion!.gravity
// We adjusted gravity vector by trial and error to get the desired effect which slows gravity in the Y direction by the specified amounts.
self.gravityBehavior.gravityDirection = CGVector(dx: CGFloat(gravityAccel.x),
dy: 0.5 * CGFloat(-gravityAccel.y / 4))
}
animator = UIDynamicAnimator(referenceView: self)
animator.addBehavior(gravityBehavior)
animator.addBehavior(collision)
}
override func willRemoveSubview(_ subview: UIView) {
guard self.emojiImageViews.removeObject(object: subview) else {
return // Subview was not an emoji image view.
}
gravityBehavior.removeItem(subview)
collision.removeItem(subview)
}
/// Resets left/right boundaries when the frame changes.
override func layoutSubviews() {
collision.removeAllBoundaries()
collision.addBoundary(withIdentifier: "left" as NSCopying,
from: CGPoint(x: -10, y: 0),
to: CGPoint(x: -10, y: (self.frame.size.height)))
collision.addBoundary(withIdentifier: "right" as NSCopying,
from: CGPoint(x: self.frame.size.width + 10, y: 0),
to: CGPoint(x: self.frame.size.width + 10, y: self.frame.size.height))
}
/// Creates and places a new confetti particle at a random position along the top of the view and adds gravity and collision behaviors to it.
internal func sprinkle() {
guard self.emojiImageViews.count < 100 else {
return // Don't add more emojis than the screen can handle.
}
let emojiImageView = UIImageView(frame: CGRect(x: generateRandomNumber(min: 0,
max: Int(self.frame.size.width)),
y: -100,
width: 51,
height: 41))
emojiImageView.image = #imageLiteral(resourceName: "logo")
self.emojiImageViews.append(emojiImageView)
// Emoji image view generated with random position along X axis
self.insertSubview(emojiImageView, at: 0)
// Add physics behaviors to newly generated confetti piece
collision.addItem(emojiImageView)
gravityBehavior.addItem(emojiImageView)
}
/// Returns a random integer between given min and max values
func generateRandomNumber(min: Int, max: Int) -> Int {
return Int(arc4random_uniform(UInt32(max) - UInt32(min)) + UInt32(min))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment