9

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()
  }
}
Ollie
  • 1,926
  • 1
  • 20
  • 35
  • what happens when you removed `self.spawn()` from `completionBlock` ? – Abdullah Md. Zubair Sep 20 '16 at 19:30
  • The shapes stop re-spawning once their animations complete. If there are animations in progress with this line removed and the next view controller is pushed the issue does not occur – Ollie Sep 20 '16 at 19:32
  • You can add a boolean flag checking to control whether to `spawn` or not. Just a thought :D – Abdullah Md. Zubair Sep 20 '16 at 19:38
  • Thanks! That's actually my current, (hopefully) temporary solution. Although I'm hoping to find the root cause of the issue. – Ollie Sep 20 '16 at 19:41
  • Welcome! .. here you are animating & end of animation on `completion` block you calling animate start again, which means you are on a `recursion` with infinite loop, that's why its animating all the time, you just need to control and stop that animation! – Abdullah Md. Zubair Sep 20 '16 at 19:49
  • http://stackoverflow.com/questions/25723401/why-changing-of-tab-stoping-the-animation-in-ios-app – Jagveer Singh Sep 26 '16 at 11:20
  • check above link it is repeated question – Jagveer Singh Sep 26 '16 at 11:20
  • @JagveerSingh Can you show me how it is a repeated question? The solution to your question was to set the `removedOnCompletion` flag to `false`. In the code accompanying my question you can see that the flag is already being set to `false` and the problem persists. – Ollie Sep 26 '16 at 11:24
  • yes sir you are right – Jagveer Singh Sep 26 '16 at 11:32

2 Answers2

2

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()
    }
}
RyuX51
  • 2,779
  • 3
  • 26
  • 33
  • Thanks for a detailed answer, I have to admit I have not worked with most of what you have suggested before. I have tried to implement your suggestions as best I can while also translating into Swift 3.0 and unfortunately the issue persists. Although if you wouldn't mind having a quick check to ensure I have converted the threading code correctly I would be very thankful. https://gist.github.com/ollie-eman/3eccd3e015a9774a31696772ef9df817 – Ollie Sep 27 '16 at 13:48
  • I adjusted my example for Swift 3. Dispatching is much more comfortable now. :) – RyuX51 Sep 27 '16 at 14:19
  • 1
    @Ollie For the code you provided on github, I think DispatchQueue.main.sync won't help you, at least try to use DispatchQueue.main.async (async vs sync) – Cristian Sep 27 '16 at 14:27
  • Forgot to mention that, yes please use async like in my example. And background thread dispatching is now as simple as `DispatchQueue.global(qos: .background).async {`. – RyuX51 Sep 27 '16 at 14:29
  • You beauty! After surrounding `self.spawn()` with `DispatchQueue.global(qos: .background).async` the CPU spike has been fixed when swapping between tabs. – Ollie Sep 27 '16 at 14:48
  • Glad to hear it. :) – RyuX51 Sep 27 '16 at 14:52
  • I removed the other stuff from the answer. – RyuX51 Sep 27 '16 at 14:59
0

In a UITabBarController, the associated view controllers have a flat structure. i.e. The view controllers in each tab run independently.

Hence, func removeAllAnimationsFromLayer() must be added in the delegate method

func tabBarController(tabBarController: UITabBarController, didSelectViewController viewController: UIViewController)

m177312
  • 1,199
  • 2
  • 14
  • 22
  • Thanks for your response, unfortunately the issue persists. I've added my implementation of your answer to the bottom of my question. If I have implemented it incorrectly please shout – Ollie Sep 27 '16 at 13:41