1

I know that there are already several questions asked about weak in the context of animations like Is it necessary to use unowned self in closures of UIView.animateWithDuration(…)? While it is obvious in the latter case, that you can omit weak, I have still difficulties, to see the reason, why I should not use the weak pattern in Robs answer about a rotating view. I do not want to disturb the comments there, so I ask the question here.

The code in question is

private func createAnimation() {
     animator = UIViewPropertyAnimator.runningPropertyAnimator(withDuration: 4, delay: 0, options: .curveLinear) { [self] in
         UIView.animateKeyframes(withDuration: 4, delay: 0) {
             UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1.0 / 3.0) {
                 animatedView.transform = .init(rotationAngle: .pi * 2 * 1 / 3)
             }
             ...
         }
     } completion: { [weak self] _ in
         self?.createAnimation()
     }
 }

Rob used the [weak self] in the completion-closure but not in the animations-closure, where he actually put self into the capture list to make his intention obvious. How can I know, that UIViewPropertyAnimator.runningPropertyAnimator will never put the (escaping) animations-closure into the created animator-instance-variable?

I don't think, that UIViewPropertyAnimator.runningPropertyAnimator actually captures the animations-closure, but as long as I have no idea, how UIViewPropertyAnimator.runningPropertyAnimator is implemented, or will be implemented in the future, how can I be sure?

Maybe this pseudo-implementation could help to explain, what I mean:

import Foundation

class UIView {
    var transform = CGFloat.zero
    static func animateKeyFrames(animations: () -> Void) {}
    static func addKeyframe(animations: () -> Void) {}
}

class UIViewPropertyAnimator {
    
    var animations: () -> Void = {}
    var completion: (() -> Void)? = {}
    
    static func runningPropertyAnimator(animations: @escaping () -> Void,
                                        completion: (() -> Void)?) -> UIViewPropertyAnimator {
        let animator = UIViewPropertyAnimator()
        animator.animations = animations
        animator.completion = completion
        
        return animator
    }
}

class ViewController {
    var animator: UIViewPropertyAnimator?
    let animatedView = UIView()
    
    func viewDidLoad() {
        createAnimation()
    }
    
    func createAnimation() {
        animator = UIViewPropertyAnimator.runningPropertyAnimator(animations: { [weak self] in
            UIView.animateKeyFrames(animations: {
                UIView.addKeyframe(animations: {
                    self?.animatedView.transform = .zero
                })
            })
        }, completion: { [weak self] in
            self?.animatedView.transform = .zero
        })
    }
    
    deinit {
        print("deinit")
    }
}

func createAndRelease() {
    let viewController = ViewController()
    viewController.viewDidLoad()
}

createAndRelease()

Removing [weak self] from the animations or completion-closure would obviously cause a retain-cycle in my pseudo-code and deinit would never be called.

andreas1724
  • 2,973
  • 1
  • 12
  • 10
  • The situation for `UIView.animate` is different from the situation for UIViewPropertyAnimator, because you might retain a UIViewPropertyAnimator. – matt Mar 31 '21 at 20:35

1 Answers1

1

Closure-based animation API have two closures, the animation closure (which is called when the animation starts and is then released) and, optionally, the completion handler closure (which is called when the animation is done).

Consider:

class ViewController: UIViewController {

    var animator: UIViewPropertyAnimator?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        let subview = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
        subview.backgroundColor = .red
        view.addSubview(subview)

        logger.debug("creating animator")
        animator = .runningPropertyAnimator(withDuration: 1, delay: 1) {
            logger.debug("animate closure called")
            subview.frame = CGRect(x: 100, y: 200, width: 100, height: 100)
        } completion: { position in
            logger.debug("completion closure called")
        }
    }
}

That will output:

2021-03-31 16:08:00.384558-0700 MyApp[3837:8759577] [ViewController] creating animator
2021-03-31 16:08:00.384899-0700 MyApp[3837:8759577] [ViewController] animate closure called
2021-03-31 16:08:02.386253-0700 MyApp[3837:8759577] [ViewController] completion closure called

Note the timestamps in the above debugging log: The animations closure is called immediately after the animator is created, and well before the completion closure is called.

In your pseudocode example, you seem to be assuming that the animator stores and retains a reference to the animations closure. It does not do that (and nor should it). It runs the animation closure to figure out what is being animated and then releases it. It does not keep a strong reference to that closure after the animation begins.

A better pseudocode example might be:

class UIViewPropertyAnimatorMockup {
    var animations: (() -> Void)?
    var completion: (() -> Void)?

    static func runningPropertyAnimator(animations: @escaping () -> Void, completion: (() -> Void)? = nil) -> UIViewPropertyAnimatorMockup {
        let animator = UIViewPropertyAnimatorMockup()
        animator.animations = animations
        animator.completion = completion
        animator.run()
        return animator
    }

    func run() {
        beginTransaction()
        animations?()
        endTransaction()

        animations = nil         // release that `animations` reference now that we're done with it
    }

    func didFinishAnimations() {
        completion?()
        completion = nil         // release that `completion` reference now that we're done with it
    }
}

This obviously is not precisely what UIViewPropertyAnimator is doing, but the idea to understand is that as soon as it calls the closure and is done with it, it releases its strong reference. This applies to both the animations closure and the completion closure (the latter obviously being released a bit later).


As an aside, this is a good pattern to follow in one’s own closure-based code. If you save a closure in some property, make sure to (a) make it optional; and (b) nil that property as soon as you are done with the closure.

This is a defensive programming pattern, mitigating damage if an application developer accidentally introduces a strong reference that they should not have.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Your pseudo-code looks much more realistic of course and all tests indicate that `UIViewPropertyAnimator` will not keep the `animations`-closure alive. But can I really be 100 percent sure about it? `UIViewPropertyAnimator` seems like a black box to me. I am am neither assuming that the animator stores and retains a reference of the `animations`-closure nor that he will never ever do it. My thought is: I simply don't know it for sure. The pure possibility, that `UIViewPropertyAnimator` **could** keep a reference of the `animations`-closure would motivate me to use `weak`. – andreas1724 Apr 01 '21 at 11:28
  • An `animations` closure obviously _must_ be called before the animation starts and it seems like a curious concern to assume that the property animator might secretly keep a reference to it beyond that, especially when you've demonstrated empirically that it doesn't. But I guess that in the lack of any formal assurances to this end, one certainly could add `[weak self]` pattern if one wanted. It seems unwarranted to my eye, only adding syntactic noise and (admittedly negligible) overhead, but to each his own. (But I wouldn't advise changing other people answers to add `[weak self]`; lol.) – Rob Apr 01 '21 at 14:24
  • 1
    Thank you for taking time to explain your opinion! And sorry: I did not intend to offend you by editing your (very helpful) answer! This was my lack of experience: It looked just like an accidental missing `weak`, so I asked you in the comments and after a few days I thought: "Sure, just an accidental missing" so I just added it too soon. – andreas1724 Apr 01 '21 at 15:25
  • No offense taken! – Rob Apr 01 '21 at 16:36