I want to animate 3 different images at specific point in time such that it behaves this way.
1) 1st image moves from (Xx, Yx) to (Xz,Yz)
2) Wait 10 seconds
3) 2nd image appears in place at Xa,Yb
4) Wait half as long as in step 2
5) Fade out 2nd image
6) 3rd image appears at the same place as 2nd image
If each of these image's animations are on their own CALayers, can I use CAKeyframeAnimation with multiple layers? If not, what's another way to go about doing staggered animations?
I'm trying to animate a playing card move from offscreen to a particular spot and then few other tricks to appear on screen several seconds later.
Edited
When I wrote this, I thought you could not use a CAAnimationGroup to animate multiple layers. Matt just posted an answer demonstrating that you can do that. I hereby eat my words.
I've taking the code in Matt's answer and adapted it to a project which I've uploaded to Github (link.)
The effect Matt's animation creates is of a pair of feet walking up the screen. I found some open source feet and installed them in the project, and made some changes, but the basic approach is Matt's. Props to him.
Here is what the effect looks like:
(The statement below is incorrect)
No, you can't use a keyframe animation to animate multiple layers. A given CAAnimation can only act on a single layer. This includes group layers, by the way.
If all you're doing is things like moving images on a straight line, fading out, and fading in, why don't you use UIView animation? Take a look at the methods who's names start with animateWithDuration:animations: Those will let you create multiple animations at the same time, and the completion block can then trigger additional animations.
If you need to use layer animation for some reason, you can use the beginTime property (which CAAnimation objects have because they conform to the CAMediaTiming protocol.) For CAAnimations that are not part of an animation group, you use
animation.beginTime = CACurrentMediaTime() + delay;
Where delay is a double which expresses the delay in seconds.
If the delay is 0, the animation would begin.
A third option would be to set your view controller up as the delegate of the animation and use the animationDidStop:finished: method to chain your animations. This ends up being the messiest approach to implement, in my opinion.
The claim that a single animation group cannot animate properties of different layers is not true. It can. The technique is to attach the animation group to the superlayer and refer to the properties of the sublayers in the individual animations' key paths.
Here is a complete example just for demonstration purposes. When launched, this project displays two "footprints" that proceed to step in alternation, walking off the top of the screen.
class ViewController: UIViewController, CAAnimationDelegate {
let leftfoot = CALayer()
let rightfoot = CALayer()
override func viewDidLoad() {
super.viewDidLoad()
self.leftfoot.name = "left"
self.leftfoot.contents = UIImage(named:"leftfoot")!.cgImage
self.leftfoot.frame = CGRect(x: 100, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.leftfoot)
self.rightfoot.name = "right"
self.rightfoot.contents = UIImage(named:"rightfoot")!.cgImage
self.rightfoot.frame = CGRect(x: 170, y: 300, width: 50, height: 80)
self.view.layer.addSublayer(self.rightfoot)
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
self.start()
}
}
func start() {
let firstLeftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
firstLeftStep.byValue = -80
firstLeftStep.duration = 1
firstLeftStep.fillMode = .forwards
func rightStepAfter(_ t: Double) -> CABasicAnimation {
let rightStep = CABasicAnimation(keyPath: "sublayers.right.position.y")
rightStep.byValue = -160
rightStep.beginTime = t
rightStep.duration = 2
rightStep.fillMode = .forwards
return rightStep
}
func leftStepAfter(_ t: Double) -> CABasicAnimation {
let leftStep = CABasicAnimation(keyPath: "sublayers.left.position.y")
leftStep.byValue = -160
leftStep.beginTime = t
leftStep.duration = 2
leftStep.fillMode = .forwards
return leftStep
}
let group = CAAnimationGroup()
group.duration = 11
group.animations = [firstLeftStep]
for i in stride(from: 1, through: 9, by: 4) {
group.animations?.append(rightStepAfter(Double(i)))
group.animations?.append(leftStepAfter(Double(i+2)))
}
group.delegate = self
self.view.layer.add(group, forKey: nil)
}
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
print("done")
self.rightfoot.removeFromSuperlayer()
self.leftfoot.removeFromSuperlayer()
}
}
Having said all that, I should add that if you are animating a core property like the position of something, it might be simpler to make it a view and use a UIView keyframe animation to coordinate animations on different views. Still, the point is that to say that this cannot be done with CAAnimationGroup is just wrong.
Related
I've created core animation using 40 images (Per image stays in CALayer exactly 3 seconds).
Total duration = 120 seconds. Each photo move from left to right with the help of CABasicAnimation(keypath : "position.x"). Now I've an UISlider. I want to update slider's value automatically when core animation is playing in CALayer. I used Timer to update slider's value like this
time = Timer.scheduledTimer(timeInterval: 1, target: self, selector: #selector(self.updateSlideValue), userInfo: nil, repeats: true)
Animation Code :
let blackLayer = CALayer()
blackLayer.frame = CGRect(x: -viewSize.width, y: 0, width: viewSize.width, height: viewSize.height)
let imageLayer = CALayer()
imageLayer.frame = CGRect(x: 0, y: 0, width: viewSize.width, height: viewSize.height)
imageLayer.contents = nextPhoto?.cgImage
blackLayer.addSublayer(imageLayer)
let animation = CABasicAnimation()
animation.keyPath = "position.x"
animation.fromValue = -viewSize.width
animation.toValue = (viewSize.width)/2
animation.duration = self.perImageLife
animation.speed = 1 // 1 is default actually
animation.beginTime = CACurrentMediaTime() + animationTimes[index-1] // animationTimes is an array like [0,3,6,9,.....]
animation.fillMode = CAMediaTimingFillMode.forwards
animation.isRemovedOnCompletion = false
animation.delegate = self
blackLayer.add(animation, forKey: "basic")
parentLayer.addSublayer(blackLayer)
But it's not a better idea to update using Timer. How to update slider's value automatically when core animation is running without Timer?
Here is a small recorded video of my project. https://drive.google.com/file/d/1SjaWJM_FEWp1TyR7fpQt8_IZdme_I2Vc/view?usp=sharing
I don't think that you can observe the properties you are animating and use those changing values to update the slider position. You'll have to use a timer to update the slider value at the moment of each frame change as you are doing.
Do you intend for the user to be able to drag the "thumb" on the slider to scrub the animation back and forth? That's another layer of complexity to the problem.
If that is your intent I would suggest using a UIViewPropertyAnimator rather than using a CAAnimation. Pausing/resuming/scrubbing a CAAnimation is tricky and not terribly well documented. I've worked it out in the past, so if you are determined to go that route I can dredge up some old sample code (probably written in Objective-C) and try to walk you through it.
This project on Github (from 7 years ago!) implements both UIView and CAAnimation-based animations that include sliders that show their progress and can scrub back and forth.
It predates UIViewPropertyAnimator however. I also wrote a much simpler demo of UIViewPropertyAnimator when it first came out. It is probably written for an older version of Swift however.
I animate the color of CAShapeLayers stored in an Array using CABasicAnimation. The animation displays erratically depending on the animation.duration and I cannot figure out why. I suspect an issue with animation.beginTime = CACurrentMediaTime() + delay
Animation Description
The animation consists in successively flashing shapes to yellow before turning them to black once the animation ends.
Current State of the animation
When the animation duration is above a certain time, it works properly.
For instance with a duration of 2 seconds:
But when I shorten the duration, the result substantially differs.
For instance, with a duration of 1 second:
You will notice that the animation has already cached/ended for the first 10 bars or so, then waits and starts animating the remainder of the shapes.
Likewise, with a duration of 0.5s:
In this case, it seems an even larger number of animation has already ended (shapes are black) before it displays some animation after a certain time. You can also notice that although the shape color animation is supposed to last the same duration (0.5s) some feels quicker than others.
The Code
The animation is called in the viewDidAppear method of the UIViewController class.
I have created a UIView custom class to draw my shapes and I animate them using an extension of the class.
The code to animate the color:
enum ColorAnimation{
case continuousSwap
case continousWithNewColor(color: UIColor)
case randomSwap
case randomWithNewColor(color: UIColor)
case randomFromUsedColors
}
func animateColors(for duration: Double,_ animationType: ColorAnimation, colorChangeDuration swapColorDuration: Double){
guard abs(swapColorDuration) != Double.infinity else {
print("Error in defining the shape color change duration")
return
}
let animDuration = abs(duration)
let swapDuration = abs(swapColorDuration)
let numberOfSwaps = Int(animDuration / min(swapDuration, animDuration))
switch animationType {
case .continousWithNewColor(color: let value):
var fullAnimation = [CABasicAnimation]()
for i in (0...numberOfSwaps) {
let index = i % (self.pLayers.count)
let fromValue = pLayers[index].pattern.color
let delay = Double(i) * swapDuration / 3
let anim = colorAnimation(for: swapDuration, fromColor: value, toColor: fromValue, startAfter: delay)
fullAnimation.append(anim)
}
for i in (0...numberOfSwaps) {
CATransaction.begin()
let index = i % (self.pLayers.count)
CATransaction.setCompletionBlock {
self.pLayers[index].shapeLayer.fillColor = UIColor.black.cgColor
}
pLayers[index].shapeLayer.add(fullAnimation[i], forKey: "fillColorShape")
CATransaction.commit()
}
default:
()
}
}
The segment the whole duration of the animation by the duration of the color change (e.g. if the whole animation is 10s and each shape changes color in 1s, it means 10 shapes will change color).
I then create the CABasicaAnimation objects using the method colorAnimation(for: fromColor, toColor, startAfter:).
func colorAnimation(for duration: TimeInterval, fromColor: UIColor, toColor: UIColor, reverse: Bool = false, startAfter delay: TimeInterval) -> CABasicAnimation {
let anim = CABasicAnimation(keyPath: "fillColor")
anim.fromValue = fromColor.cgColor
anim.toValue = toColor.cgColor
anim.duration = duration
anim.autoreverses = reverse
anim.beginTime = CACurrentMediaTime() + delay
return anim
}
Finally I add the animation to the adequate CAShapeLayer.
The code can obviously be optimized but I chose to proceed by these steps to try to find why it was not working properly.
Attempts so far
So far, I have tried:
with and without setting the animation.beginTime in the colorAnimation method, including with and without CACurrentMediaTime(): if I don't set the animation.beginTime with CACurrentMediaTime, I simply do not see any animation.
with and without pointing animation.delegate = self: it did not change anything.
using DispatchQueue (store the animations in global and run it in main) and as suspected, the shapes did not animate.
I suspect something is not working properly with the beginTime but it might not be the case, or only this because even when the shapes animate, the shape animation duration seems to vary whilst it should not.
Thank very much in advance to have a look to this issue. Any thoughts are welcome even if it seems far-fetched it can open to new ways to address this!
Best,
Actually there is a relationship between duration and swapColorDuration
func animateColors(for duration: Double,_ animationType: ColorAnimation, colorChangeDuration swapColorDuration: Double)
when you call it, you may need to keep this relationship
let colorChangeDuration: TimeInterval = 0.5
animateColors(for: colorChangeDuration * TimeInterval(pLayers.count), .continousWithNewColor(color: UIColor.black), colorChangeDuration: colorChangeDuration)
Also here :
let numberOfSwaps = Int(animDuration / min(swapDuration, animDuration)) - 1
This value maybe a little higher than you need.
or
The problem lies in this let index = i % (self.pLayers.count)
if numberOfSwaps > self.pLayers.count, some bands will be double animations.
let numberOfSwaps1 = Int(animDuration / min(swapDuration, animDuration))
let numberOfSwaps = min(numberOfSwaps1, self.pLayers.count)
in the rest is
for i in (0..<numberOfSwaps) {... }
Now if numberOfSwaps < self.pLayers.count. It's not finished.
if numberOfSwaps is larger, It is fine.
If double animations are required, changes the following:
pLayers[index].shapeLayer.add(fullAnimation[i], forKey: nil)
or pLayers[index].shapeLayer.add(fullAnimation[i], forKey: "fillColorShape" + String(i))
I'd like to animate a drawing sequence. My code draws a spiral into a UIImageView.image. The sequence changes the image contents, but also changes the scale of the surrounding UIImageView. The code is parameterized for the number of revolutions of the spiral:
func drawSpiral(rotations:Double) {
let scale = scaleFactor(rotations) // do some math to figure the best scale
UIGraphicsBeginImageContextWithOptions(mainImageView.bounds.size, false, 0.0)
let context = UIGraphicsGetCurrentContext()!
context.scaleBy(x: scale, y: scale) // some animation prohibits changes!
// ... drawing happens here
myUIImageView.image = UIGraphicsGetImageFromCurrentImageContext()
}
For example, I'd like to animate from drawSpiral(2.0) to drawSpiral(2.75) in 20 increments, over a duration of 1.0 seconds.
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values? How? Is there a better animation approach?
Can I setup UIView.annimate(withDuration...) to call my method with successive intermediate values
Animation is merely a succession of timed intermediate values being thrown at something. It is perfectly reasonable to ask that they be thrown at your code so that you can do whatever you like with them. Here's how.
You'll need a special layer:
class MyLayer : CALayer {
#objc var spirality : CGFloat = 0
override class func needsDisplay(forKey key: String) -> Bool {
if key == #keyPath(spirality) {
return true
}
return super.needsDisplay(forKey:key)
}
override func draw(in con: CGContext) {
print(self.spirality) // in real life, this is our signal to draw!
}
}
The layer must actually be in the interface, though it can be impossible for the user to see:
let lay = MyLayer()
lay.frame = CGRect(x: 0, y: 0, width: 1, height: 1)
self.view.layer.addSublayer(lay)
Subsequently, we can initialize the spirality of the layer:
lay.spirality = 2.0
lay.setNeedsDisplay() // prints: 2.0
Now when we want to "animate" the spirality, this is what we do:
let ba = CABasicAnimation(keyPath:#keyPath(MyLayer.spirality))
ba.fromValue = lay.spirality
ba.toValue = 2.75
ba.duration = 1
lay.add(ba, forKey:nil)
CATransaction.setDisableActions(true)
lay.spirality = 2.75
The console shows the arrival of a succession of intermediate values over the course of 1 second!
2.03143266495317
2.04482554644346
2.05783333256841
2.0708108600229
2.08361491002142
2.0966724678874
2.10976020619273
2.12260236591101
2.13551922515035
2.14842618256807
2.16123360767961
2.17421661689878
2.18713565543294
2.200748950243
2.21360073238611
2.2268518730998
2.23987507075071
2.25273013859987
2.26560932397842
2.27846492826939
2.29135236144066
2.30436328798532
2.31764804571867
2.33049770444632
2.34330793470144
2.35606706887484
2.36881992220879
2.38163591921329
2.39440815150738
2.40716737508774
2.42003352940083
2.43287514150143
2.44590276479721
2.45875595510006
2.47169743478298
2.48451870679855
2.49806520342827
2.51120449602604
2.52407149970531
2.53691896796227
2.54965999722481
2.56257836520672
2.57552136480808
2.58910304307938
2.60209316015244
2.6151298135519
2.62802086770535
2.64094598591328
2.6540260463953
2.6669240295887
2.6798157542944
2.69264766573906
2.70616912841797
2.71896715462208
2.73285858333111
2.74564798176289
2.75
2.75
2.75
Those are exactly the numbers that would be thrown at an animatable property, such as when you change a view's frame origin x from 2 to 2.75 in a 1-second duration animation. But now the numbers are coming to you as numbers, and so you can now do anything you like with that series of numbers. If you want to call your method with each new value as it arrives, go right ahead.
Personally, in more complicated animations I would use lottie the animation itself is built with Adobe After Effect and exported as a JSON file which you will manage using the lottie library this approach will save you time and effort when you port your app to another platform like Android as they also have an Android Lottie which means the complicated process of creating the animation is only done once.
Lottie Files has some examples animations as well for you to look.
#Matt provided the answer and gets the checkmark. I'll recap some points for emphasis:
UIView animation is great for commonly animated properties, but if
you need to vary a property not on UIView's animatable list, you can't use it. You must
create a new CALayer and add a CABasicAnimation(keyPath:) to it.
I tried but was unable to get my CABasicAnimations to fire by adding them to the default UIView.layer. I needed to create a custom CALayer
sublayer to the UIView.layer - something like
myView.layer.addSublayer(myLayer)
Leave the custom sublayer installed and re-add the CABasicAnimation to that sublayer when (and only when) you want to animate drawing.
In the custom CALayer object, be sure to override class func needsDisplay(forKey key: String) -> Bool with your key property (as #Matt's example shows), and also override func draw(in cxt: CGContext) to do your drawing. Be sure to decorate your key property with #objc. And reference the key property within the drawing code.
A "gotcha" to avoid: in the UIView object, be sure to null out the usual draw method (override func draw(_ rect: CGRect) { }) to avoid conflict between animated and non-animated drawing on the separate layers. For coordinating animated and non-animated content in the same UIView, it's good (necessary?) to do all your drawing from your custom layer.
When doing that, use myLayer.setNeedsDisplay() to update the non-animated content within the custom layer; use myLayer.add(myBasicAnimation, forKey:nil) to trigger animated drawing within the custom layer.
As I said above, #Matt answered - but these items seemed worth emphasizing.
So my goal is to make a sort of sliding door animation in response to a swipe gesture. You can see a GIF of my current animation here (ignore the fact that the gesture behaves opposite to what you'd expect).
Here's how I'm currently accomplishing this: I have a subclass of UIView I'm calling DoorView. DoorView has three CALayers: the base superlayer that comes with every UIView; a sublayer called doorLayer which is the white rectangle that slides; and another sublayer called frameLayer which is the "doorframe" (the black border around doorLayer). The doorLayer and the frameLayer have their own separate animations that are triggered in sequence.
Here's what I need to add to DoorView: a simple rectangle that represents a door handle. At the moment I don't plan to give the door handle its own animation. Instead, I want it to simply be "attached" to the doorLayer so that it animates along with any animations applied to doorLayer.
This is where my first question comes in: I know that I can add another layer (let's call it handleLayer) and add it as a sublayer to doorLayer. But is there a way to simply "draw" a small rectangle on doorLayer without needing an extra layer? And if so, is this preferable for any reason?
Now for my second question: so at the moment I am in fact using a separate layer called handleLayer which is added as a sublayer to doorLayer. You can see a GIF of the animation with the handleLayer here.
And here is the animation being applied to doorLayer:
UIView.animateWithDuration(1.0, animations: { () -> Void in
self.doorLayer.frame.origin.x = self.doorLayer.frame.maxX
self.doorLayer.frame.size.width = 0
}
This animation shifts the origin of doorLayer's frame to the door's right border while decrementing its width, resulting in the the appearance of a door sliding to the right and disappearing as it does so.
As you can see in the above GIF, the origin shift of doorLayer is applied to its handleLayer sublayer, as desired. But the width adjustment does not carry over to the handleLayer. And this is good, because I don't want the handle to be getting narrower at the same rate as the doorLayer.
Instead what is desired is that the handleLayer moves with the doorLayer, but retains its size. But when the doorLayer disappears into the right side of the doorframe, the handle disappears with it (as it would look with a normal door). Any clue what the best way to accomplish this is?
Currently in my doorLayer's animation, I added this line:
if self.doorLayer.frame.size.width <= self.handleLayer.frame.size.width {
self.handleLayer.frame.size.width = 0
}
But that results in this, which isn't quite right.
Thanks for any help!
From a high level, you would need to
Make your sliding layer a child of your outline layer
Make your outline layer masks its bounds
Animate your sliding layer's transform using a x translation
On completion of the animation, animate your outline layer's transform using a scale translation
Reverse the animations to close the door again
Your doorknob layer is fine as is and no need to animate it separately.
I took a shot at it for fun and here's what I came up with. I didn't use a swipe gesture, but it could just as easily by added. I trigger the animation with a tap on the view. Tap again to toggle back.
func didTapView(gesture:UITapGestureRecognizer) {
// Create a couple of closures to perform the animations. Each
// closure takes a completion block as a parameter. This will
// be used as the completion block for the Core Animation transaction's
// completion block.
let slideAnimation = {
(completion:(() -> ())?) in
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(1.0)
if CATransform3DIsIdentity(self.slideLayer.transform) {
self.slideLayer.transform = CATransform3DMakeTranslation(220.0, 0.0, 0.0)
} else {
self.slideLayer.transform = CATransform3DIdentity
}
CATransaction.commit()
}
let scaleAnimation = {
(completion:(() -> ())?) in
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
CATransaction.setAnimationDuration(1.0)
if CATransform3DIsIdentity(self.baseLayer.transform) {
self.baseLayer.transform = CATransform3DMakeScale(2.0, 2.0, 2.0)
} else {
self.baseLayer.transform = CATransform3DIdentity
}
CATransaction.commit()
}
// Check to see if the slide layer's transform is the identity transform
// which would mean that the door is currently closed.
if CATransform3DIsIdentity(self.slideLayer.transform) {
// If the door is closed, perform the slide animation first
slideAnimation( {
// And when it completes, perform the scale animation
scaleAnimation(nil) // Pass nil here since we're done animating
} )
} else {
// Otherwise the door is open, so perform the scale (down)
// animation first
scaleAnimation( {
// And when it completes, perform the slide animation
slideAnimation(nil) // Pass nil here since we're done animating
})
}
}
Here's how the layers are setup initially:
func addLayers() {
baseLayer = CALayer()
baseLayer.borderWidth = 10.0
baseLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 220, height: 500.0)
baseLayer.masksToBounds = true
baseLayer.position = self.view.center
slideLayer = CALayer()
slideLayer.bounds = baseLayer.bounds
slideLayer.backgroundColor = UIColor.whiteColor().CGColor
slideLayer.position = CGPoint(x: baseLayer.bounds.size.width / 2.0, y: baseLayer.bounds.size.height / 2.0)
let knobLayer = CALayer()
knobLayer.bounds = CGRect(x: 0.0, y: 0.0, width: 20.0, height: 20.0)
knobLayer.cornerRadius = 10.0 // Corner radius with half the size of the width and height make it round
knobLayer.backgroundColor = UIColor.blueColor().CGColor
knobLayer.position = CGPoint(x: 30.0, y: slideLayer.bounds.size.height / 2.0)
slideLayer.addSublayer(knobLayer)
baseLayer.addSublayer(slideLayer)
self.view.layer.addSublayer(baseLayer)
}
And here's what the animation looks like:
You can see a full Xcode project here: https://github.com/perlmunger/Door
I've been trying to learn and understand the emitter functions of CAEmitter, but I'm currently a little bit stuck. I want to add an image for the emitter and make it stop after a duration.
I've got a view that I'm using to emit some particles, and I want them to only appear emit when the view appears for around 10 seconds, then stop. I also am unsure how to attach a UI image with a png, instead of using CGrect.
Thanks for any help and advice!
import UIKit
class ParticleView: UIView {
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
override func drawRect(rect: CGRect) {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(15,8), false, 1)
let con = UIGraphicsGetCurrentContext()
CGContextAddRect(con, CGRectMake(0, 0, 15, 8))
CGContextSetFillColorWithColor(con, UIColor.whiteColor().CGColor)
CGContextFillPath(con)
let im = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
// make a cell with that image
var cell = CAEmitterCell()
cell.birthRate = 10
cell.color = UIColor(red:0.5, green:0.5, blue:0.5, alpha:1.0).CGColor
cell.redRange = 1
cell.blueRange = 1
cell.greenRange = 1
cell.lifetime = 5
cell.alphaSpeed = -1/cell.lifetime
cell.velocity = -100
cell.spinRange = 10.0
cell.scale = 1.0;
cell.scaleRange = 0.2;
cell.emissionRange = CGFloat(M_PI)/5.0
cell.contents = im.CGImage
var emit = CAEmitterLayer()
emit.emitterSize = CGSize(width: 100, height: 0)
emit.emitterPosition = CGPointMake(30,100)
emit.emitterShape = kCAEmitterLayerLine
emit.emitterMode = kCAEmitterLayerLine
emit.emitterCells = [cell]
self.layer.addSublayer(emit)
}
}
I have found a nice workaround to stop CAEmitter:
Create 2 identical view controllers with the same layout
Implement a Start and Stop button on both (to begin and end the CAEmitter)
Connect the Stop button of each view controller to each other with a “Show Detail (e.g. Replace) Segue and deselect "Animates"
When you hit “Stop” button it will make a seamless transition to the identical VC without emitter particles!! But what is really happening is that you are just switching to a replica view controller. This is not an elegant solution but it is the only reliable way that I have found to stop a CAEmitter (segue to a different VC) all of the other solutions are "buggy"
visual of how the VCs are set up