Categories
CoreAnimation iOS Swift

Particle emitters for apps with CAEmitterLayer on iOS

Particle emitters are encountered the most often in games. Apple’s SpriteKit framework contains SKEmitterNode for this exact purpose. Moreover, Xcode even has a live editor for configuring SKEmitterNode. As SKEmitterNode can only be added to SKScene, it is not a good fit for apps. But no worries, apps can take advantage of CAEmitterLayer which is part of the CoreAnimation framework. CAEmitterLayer is subclass of CALayer and therefore we can add it to anywhere in our UIKit based apps. Snowboarding and skiing related apps could add a cool snow effect with a little bit of code.

CAEmitterLayer and CAEmitterCell

Particles used by CAEmitterLayer are defined by CAEmitterCell. Every emitter layer can have multiple cells and every cell can have it’s own subcells as well. Meaning every particle has an ability of emitting other particles. CAEmitterCell itself is just a definition of the particle: how it moves, looks and lives.

extension CAEmitterLayer {
convenience init(image: CGImage) {
self.init()
let cell: CAEmitterCell = {
let cell = CAEmitterCell()
cell.birthRate = 40
cell.contents = image
cell.emissionLongitude = .pi / 2.0
cell.emissionRange = CGFloat.pi / 4.0
cell.lifetime = 16
cell.scale = 0.3
cell.scaleRange = 0.2
cell.velocity = 80
return cell
}()
emitterCells = [cell]
}
}

This example presents a way of how to create a simple CAEmitterLayer what only has one cell with predefined image. Here we can see that cell defines birthrate: (particles per second), scale and its variance, lifetime in seconds, velocity, and emission direction. Emission longitude and range define a cone in which particles are emitted: if range is zero, all the particles have the same direction (emissionLongitude) and if range is 2 * pi, then particles are emitted in all the directions.

CAEmitterLayer also several different emitter shapes: circle, cuboid, line, point, rectangle, sphere (checkout the example project down below). It should be noted those shapes are defined by emitterPosition, emitterDepth, emitterSize, emitterZPosition and therefore, if you want to move emission location, update emitterPosition, not layer’s position.

Adding emitter layer

On iOS every UIView is backed by an instance of CALayer. As CAEmitterLayer is also a layer then adding it to view hierarchy is as simple as adding a sublayer to view’s backing layer.

override func viewDidLoad() {
super.viewDidLoad()
emitterView.layer.addSublayer({
guard let image = UIImage(named: "Spark")?.cgImage else { fatalError("Failed loading image.") }
let emitterLayer = CAEmitterLayer(image: image)
emitterLayer.emitterPosition = emitterView.bounds.center
emitterLayer.name = "Emitter"
return emitterLayer
}())
}
extension CGRect {
var center: CGPoint {
return CGPoint(x: midX, y: midY)
}
}

Summary

CAEmitterLayer is an efficient way of adding particle systems to any app. Setting up emitter layer required to define particle(s) and then adding the CAEmitterLayer to the view hierarchy. With a little bit of code, we can add playful touch to any app. Only if I could design an awesome looking particle emitter. 🤔

If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.

Example project

CAEmitterLayer (GitHub), Xcode 10.1, Swift 4.2

Resources

Categories
CoreAnimation iOS Swift UIKit

Custom non-interactive transition in iOS

In iOS view transitions can be interactive and non-interactive. In this post we are going to take a look on how to implement a custom non-interactive transition.

Setting up a custom transition

For setting up a custom non-interactive transition it is needed to create an animator object defining the transition and feeding it into UIKit. Before view controller is presented, we’ll need to change the UIModalPresentationStyle to custom, set delegate and with delegate method providing the custom animator to UIKit.

final class ViewController: UIViewController, UIViewControllerTransitioningDelegate {
@objc func showView() {
let presentedViewController = PresentedViewController()
presentedViewController.modalPresentationStyle = .custom
presentedViewController.transitioningDelegate = self
present(presentedViewController, animated: true, completion: nil)
}
private let transition = CustomTransition()
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
return transition
}
}

Custom animator

Custom animator object needs to conform to UIViewControllerContextTransitioning. It is required to implement a method defining the duration of the transition and method performing the transition. UIKit calls those methods and provides a UIViewControllerContextTransitioning object what gives contextual information about the transition (e.g. view controllers related to the transition). It is important to check isAnimated property for seeing if the transition should be animated at all. Secondly, it is required to call completeTransition() when transition has finished.
Let’s take a look on an example implementation of custom transition. In this particular case Core Animation is used for implementing animations. Several animations run in an animation group, and when it finishes, completeTransition() is called. Core Animation is used because of the need to rotate the presented view which is easy to do with CABasicAnimation. Just for keeping in mind that most of the simpler animations might be easier just to implement with UIView’s animate(withDuration:delay:options:animations:completion:).

final class CustomTransition: NSObject, CAAnimationDelegate, UIViewControllerAnimatedTransitioning {
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return 1.0
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
guard let toViewController = transitionContext.viewController(forKey: .to) else { return }
transitionContext.containerView.addSubview(toViewController.view)
if transitionContext.isAnimated {
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
let opacity: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "opacity")
animation.duration = transitionDuration(using: transitionContext)
animation.fromValue = 0.0
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
animation.toValue = 1.0
return animation
}()
let rotation: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.rotation")
animation.duration = transitionDuration(using: transitionContext)
animation.fromValue = 0.0
animation.toValue = 2.0 * 2.0 * Double.pi
animation.timingFunction = CAMediaTimingFunction(name: .easeIn)
return animation
}()
let scale: CABasicAnimation = {
let animation = CABasicAnimation(keyPath: "transform.scale")
animation.duration = transitionDuration(using: transitionContext)
animation.fromValue = 0.1
animation.toValue = 1.0
animation.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
return animation
}()
let group: CAAnimationGroup = {
let group = CAAnimationGroup()
group.animations = [opacity, rotation, scale]
group.delegate = self
group.duration = transitionDuration(using: transitionContext)
return group
}()
self.transitionContext = transitionContext
toViewController.view.layer.add(group, forKey: "rotateScaleGroup")
}
else {
toViewController.view.frame = transitionContext.finalFrame(for: toViewController)
transitionContext.completeTransition(true)
}
}
private var transitionContext: UIViewControllerContextTransitioning? = nil
func animationDidStop(_ animation: CAAnimation, finished isFinished: Bool) {
transitionContext?.completeTransition(isFinished)
transitionContext = nil
}
}

Here is the end result.
CustomViewTransitionExample

Summary

In this blog post we took a look on how to use custom transitions when presenting a view controller. It was a matter of setting presentation style to custom and creating and providing an animator object to UIKit using a delegate.

Playground

CustomViewTransition (GitHub) Xcode 10, Swift 4.2

References

UIViewControllerContextTransitioning (Apple)