iosswiftcatransaction

Animations not stopping after view controller is dismissed using tab bar


The Problem
I have two view controllers, both are contained within respective UINavigationControllers and a single UITabBarController. On one of the view controllers I am creating a bubbles effect, where I draw bubbles on the screen and animate their positions. The problem occurs when I move to the other view controller using the tab bar, this causes the CPU to spike and remain at 100% and the bubbles to continue to animate.

Code
The code for the bubbles is encapsulated within a UIView subclass.

override func draw(_ rect: CGRect) {
    // spawn shapes
    for _ in 1 ... 10 { // spawn 75 shapes initially
      spawn()
    }
  }

The drawRect method repeatedly calls the spawn function to populate the view with bubbles.

fileprivate func spawn() {
    let shape = CAShapeLayer()
    shape.opacity = 0.0

    // create an inital path at the starting position
    shape.path = UIBezierPath(arcCenter: CGPoint.zero, radius: 1, startAngle: 0, endAngle: 360 * (CGFloat.pi / 180), clockwise: true).cgPath
    shape.position = CGPoint.zero

    layer.addSublayer(shape)


    // add animation group
    CATransaction.begin()

    let radiusAnimation = CABasicAnimation(keyPath: "path")
    radiusAnimation.fromValue = shape.path
    radiusAnimation.toValue = UIBezierPath(arcCenter: center, radius: 100, startAngle: 0, endAngle: 360 * (CGFloat.pi / 180), clockwise: true).cgPath

    CATransaction.setCompletionBlock { [unowned self] in

      // remove the shape
      shape.removeFromSuperlayer()
      shape.removeAllAnimations()

      // spawn a new shape
      self.spawn()
    }

    let movementAnimation = CABasicAnimation(keyPath: "position")
    movementAnimation.fromValue = NSValue(cgPoint: CGPoint.zero)
    movementAnimation.toValue = NSValue(cgPoint: CGPoint(x: 100, y: 100))


    let animationGroup = CAAnimationGroup()
    animationGroup.animations = [radiusAnimation, movementAnimation]
    animationGroup.fillMode = kCAFillModeForwards
    animationGroup.isRemovedOnCompletion = false
    animationGroup.duration = 2.0

    shape.add(animationGroup, forKey: "bubble_spawn")

    CATransaction.commit()
  }

Within the CATransaction completion handler I remove the shape from the superview and create a new one. The function call to self.spawn() seems to be the problem

On viewDidDisappear of the containing view controller I call the following:

func removeAllAnimationsFromLayer() {

    layer.sublayers?.forEach({ (layer) in
      layer.removeAllAnimations()
      layer.removeFromSuperlayer()
    })

    CATransaction.setCompletionBlock(nil)
  }

Attempts from answers
I've tried to add the removeAllAnimations function to the UITabBarControllerDelegate

extension BaseViewController: UITabBarControllerDelegate {

  func tabBarController(_ tabBarController: UITabBarController, didSelect viewController: UIViewController) {

    bubblesView.removeAllAnimationsFromLayer()
  }
}

Solution

  • I think your problem is, that you only use one thread for all that stuff. Please play around with dispatching everything that affects your GUI to the main thread and maybe explicitly new spawn instances to other threads. See how that goes. Something like this:

    fileprivate func spawn() {
    
        let shape = CAShapeLayer()
        shape.opacity = 0.0
    
        // create an inital path at the starting position
        shape.path = UIBezierPath(arcCenter: CGPoint.zero, radius: 1, startAngle: 0, endAngle: 360 * (CGFloat.pi / 180), clockwise: true).cgPath
        shape.position = CGPoint.zero
    
    
        // create an inital path at the starting position
        shape.path = UIBezierPath(arcCenter: startingPosition, radius: startRadius, startAngle: BubbleConstants.StartingAngle, endAngle: BubbleConstants.EndAngle, clockwise: true).cgPath
        shape.position = startingPosition
    
        // set the fill color
        shape.fillColor = UIColor.white.cgColor
    
        layer.addSublayer(shape)
    
        shape.opacity = Float(opacity)
    
        DispatchQueue.main.async {
            self.layer.addSublayer(shape)
            CATransaction.begin()
        }
    
        let radiusAnimation = CABasicAnimation(keyPath: "path")
        radiusAnimation.fromValue = shape.path
        radiusAnimation.toValue = UIBezierPath(arcCenter: center, radius: endRadius, startAngle: BubbleConstants.StartingAngle, endAngle: BubbleConstants.EndAngle, clockwise: true).cgPath
    
    
        DispatchQueue.main.async { [unowned self] in
            CATransaction.setCompletionBlock { [unowned self] in
    
                // remove the shape
                DispatchQueue.main.async {
                    shape.removeFromSuperlayer()
                    shape.removeAllAnimations()
                }
    
                DispatchQueue.global(qos: .background).async {
                    // spawn a new shape
                    self.spawn()
                }
            }
        }
    
    
        let movementAnimation = CABasicAnimation(keyPath: "position")
        movementAnimation.fromValue = NSValue(cgPoint: startingPosition)
        movementAnimation.toValue = NSValue(cgPoint: destination)
    
    
        let animationGroup = CustomAnimationGroup()
        animationGroup.animations = [radiusAnimation, movementAnimation]
        animationGroup.fillMode = kCAFillModeForwards
        animationGroup.isRemovedOnCompletion = false
        animationGroup.duration = duration
    
        shape.add(animationGroup, forKey: "bubble_spawn")
    
        DispatchQueue.main.async {
            CATransaction.commit()
        }
    }