Last active
September 15, 2017 03:16
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// 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