Skwiggs
Skwiggs

Reputation: 1436

UIView.animate called in didMoveToSuperview unexpectedly affects all subviews

I'm working on a custom UIView. I got it all working properly, but my design requirements dictate that parts of the view should animate upon load.

My view is set in the following way, and I chose to animate constraints: UIView layout

So I called UIView.animate() in didMoveToSuperview() like such:

override func didMoveToSuperview() {
    animateArrow()
}

private func animateArrow() {
    UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .autoreverse, .repeat], animations: {
        self.arrowLeadingConstraint.constant += 15
        self.layoutIfNeeded()
    }, completion: nil)
}

I'm not doing anything else. On its own, the animation only affects the leading constraint of my arrow image view. As expected, and as it should. I can verify this when I start the animation upon user interaction, as pictured below.

Arrow animation on user interaction

Now, the problem is, when called from within didMoveToSuperview(), the animation somehow affects all subviews of my custom UIView...

Animation on didMoveToSuperview

What am I doing wrong ?

Upvotes: 0

Views: 711

Answers (3)

Amber K
Amber K

Reputation: 698

Call the animation code here:

override func draw(_ rect: CGRect) {
    super.draw(rect)
    animateArrow()
}

Or

as suggested by @holex comment: perform a layout pass before:

private func animateArrow() { 
   self.layoutIfNeeded(); 
   self.arrowLeadingConstraint.constant = 31; 
   UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .autoreverse, .repeat]) { 
       self.layoutIfNeeded() 
   } 
}

Also add an observer in your init if animation stops on pressing home:

NotificationCenter.default.addObserver(self, selector: #selector(enteredForeground(_:)), name: .UIApplicationWillEnterForeground, object: nil)

Upvotes: 1

holex
holex

Reputation: 24041

it seems the devil is in your animateArrow() method itself, if you amend the method a little bit like e.g. this:

private func animateArrow() {
    self.layoutIfNeeded()
    self.arrowLeadingConstraint.constant = 31 // = 15 + 16 from your original code

    UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .autoreverse, .repeat], animations: {

        self.layoutIfNeeded()
    }, completion: nil)
}

tada, the animation will work properly as you expected.


why...?

my explanation may not be academic here but I hope it will make sense to the readers for getting a better understanding.

so, briefly, when you are dealing with constraints you are implicitly dealing with a set of predefined relationships between the view and its surroundings. that is why you cannot animate an individual constraint successfully (your original attempt) because only these relationships are animatable in this context – not the constraints.

therefore you will be able to animate the update of all relationships only after you defined the new constraint(s) for your layout – and in principle behind the scenes that could lead to animate every affected view's frame for you in one go.

you can read more about what the constraints are and how the evaluation works with Auto-Layout from Apple, if you are interested in that.

Upvotes: 1

marosoaie
marosoaie

Reputation: 2371

Using didMoveToSuperview might not be the best idea. Before starting the animation you need to make sure that the layout for all the views on the screen has been done, which might not always be true in didMoveToSuperview.

I would move the animation trigger inside viewDidAppear in the viewController or in didLayoutSubviews which is also in the viewcontroller.

Upvotes: 1

Related Questions