Ollie Ollie - 2 months ago 15
iOS Question

Animations not stopping after view controller is dismissed using tab bar

The Problem

I have two view controllers, both are contained within respective

UINavigationController
s 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()
}
}

Answer

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()
    }
}
Comments