UIView constraints won't animate if layoutIfNeeded() is called - ios

I'm trying to animate a view that is constrained (iconVerticalConstraint) to be at center Y (+100) of a superview, specifically reaching its right (0) position after an animation.
Here's my code:
self.view.layoutIfNeeded()
UIView.animateKeyframes(withDuration: 2, delay: 0, animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations:{
self.iconVerticalConstraint.constant -= 20
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations:{
self.iconVerticalConstraint.constant -= 80
})
})
I tried following this question's answer but that didn't help, because in my case my view contains more subviews, intrinsically linked, and putting layoutIfNeeded() in my animation block would animate the whole view, and not just that constraint. Otherwise this approach does no animation at all.

changing constraints such as
self.iconVerticalConstraint.constant -= 20
should be OUTSIDE the animation block and inside you just call
self.view.layoutIfNeeded()
I think because you are trying to do 2, and calling
self.view.layoutIfNeeded()
They all happen once, consider changing to:
self.iconVerticalConstraint.constant -= 20
UIView.animate(withDuration: 0.25, animations: {
self.view.layoutIfNeeded()
}) { (finished) in
self.iconVerticalConstraint.constant -= 80
UIView.animate(withDuration: 0.75, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
}
Edit: Explanation
When you change a constraint, the view will not update until the next layout cycle, usually when you exit your current scope. So when you want to animate a constraint, you change the constant BEFORE the animation block, then inside the animation block you call layoutIfNeeded() This tells it to animate the now forced layout cycle.
Now in your example, you where calling layoutIfNeeded() which would do nothing at that point, then changing 2 constants, without telling the layout to animate, so when the scope exited, the UI would layout without animation.
In your case you want to chain the changing of 2 constants, but you dont have a way of changing the second constant (80) after the first animation completes, and if you set both, both constants will be adjusted on the FIRST layoutIfNeeded() hence, the chaining of animations, which will still work with your other animations that you omitted, just add then in the first animation block if they are in the first keyframe and the second if they were in the second.
Final Edit:
Also you can change your layoutIfNeeded() currently applied to self.view to actually on the views it affects, in this case iconVerticalConstraint if it was a view that is attached to another view say a top attached to a view bottom, if you wanna animate, you must call layoutIfNeeded() on each view it affects

After hours of time spent to understand the basic mechanism, I can provide my own answer, for all of you who will find themselves in my situation.
layoutIfNeeded() tells to the view to adjust position of all of its subviews.
animate or animateKeyframes or every other alternative you're using, instead, tells to the view to perform any of the instructions inside the animation block in the way you specify in the animate function (by default, with a options: .easeInOut)
Back to the question: the problem was in the fact that animate and layoutIfNeeded are called asynchronously so that, in fact, every instruction inside the animation block was executed just before the layoutIfNeeded instruction, making it useless. The solution isn't putting the layoutIfNeeded instruction inside the animation block, as suggested by SeanLintern88 because in this way we would tell the view to adjust the position of its subviews according to the animation options. That's the key point.
The answer is somewhat simple. Move the animation part on the subview itself, specifically in a point when you know that the superview has already been "autolayouted" (e.g. viewDidAppear), so that the constraint is already defined and executed, and you can now change it. In that point, code should look like this:
UIView.animateKeyframes(withDuration: 2, delay: 0, animations: {
self.view.layoutIfNeeded()
UIView.addKeyframe(withRelativeStartTime: 0.0, relativeDuration: 0.25, animations:{
self.iconVerticalConstraint.constant -= 20
})
UIView.addKeyframe(withRelativeStartTime: 0.25, relativeDuration: 0.75, animations:{
self.iconVerticalConstraint.constant -= 80
})
})
Because you want to animate the autolayout.
Another option (for instance, if you haven't/don't want to create a class for your subview) is to change bounds/center of the subview itself in the viewDidAppear function of the parent view. This is because, if you are in the parent view, you know that at that point auto layout has already been registered but not executed, in the more precise sense that bounds and center have been already changed according to the auto layout.
If you then change them, you'll obtain your desired animation (but remember: auto layout is still on, so you must change the constraints in a way that reflects the final position of your animation; if you wish, you can do that in the completion part of animate function).

Related

Why do these constraints seem to lag when animated?

I am animating my constraints using UIView.animate. drawerHeight is a constraint variable.
Here is the code:
drawerHeight?.constant = newHeight
UIView.animate(withDuration: 0.35, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 2, options: .allowUserInteraction, animations: {
self.superview?.layoutIfNeeded()
})
Here is a video of what the issue looks like.
Looking at the bottom of the video, the bottom edge appears to lag behind. Why do these constraints seem to lag when animated?
This is a self-answered question. This post is to help people in the future discover what they did wrong, and how to quickly fix it.
The issue was that the constraint was attached to a view in a deeper hierarchy than before. When using layoutIfNeeded(), it should be called on the superview of the view containing the constraints.
In my situation, I put the view with the drawerHeight constraint within another UIView, which made it deeper in the hierarchy.
I fixed the issue by doing
self.superview?.superview?.layoutIfNeeded() ,
instead of
self.superview?.layoutIfNeeded() .

How to do interactive transition + ending animation?

Normally, we can do an interactive transitioning with animateTransition of UIViewControllerAnimatedTransitioning and updating progress via UIPercentDrivenInteractiveTransition.
Question:
How to have the interactive transitioning at first, then as we pass a certain threshold, perform a different ending animation?
What I want to achieve here is something like dismissing App store's Today card (https://gph.is/2qgcGHd). We can interactively shrink the card by panning a left edge of the screen. Then when it reaches the point, the card animates back to home page without any interactivity. It seems like a combination of interactive + animate transition to me.
What I've tried:
I tried doing this in UIView.animateWithKeyFrames by dividing into two parts of animation with 0.5 relative times for each. Then as the progress reach 0.5, I call finish() (of UIPercentDrivenInteractiveTransition) to have the second animation performing. It has some glitches there and it's like a hack. Want to know if there's a better way to do this.
In the end, I use UIView.animateKeyFramesand dividing the interactive transition into two-part animation (as explained in the question):
let progressUntilDismissing = 0.4
UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: [], animations: {
UIView.addKeyframe(withRelativeStartTime: 0.0,
relativeDuration: progressUntilDismissing,
animations: {
// interactive dismissing animation...
})
UIView.addKeyframe(withRelativeStartTime: progressUntilDismissing,
relativeDuration: (1 - progressUntilDismissing),
animations: {
// closing dismissing animation...
})
}) { (finished) in
//...
}
Then in the pan gesture recognizer, I calculate the pan progress and determine if it passes progressUntilDismissing or not.
If yes, call finish() on UIPercentDrivenInteractiveTransition subclass, it will animate the closing dismissing animation automatically.
In case anyone is curious, this is what I'm playing with:
AppStoreTodayInteractiveTransition

swift 3 layoutIfneeded not working properly

To make proper animation of a uiview with constraints u must set the new constraint value and then call theView.layoutIfNeeded() , for swift 3 this doesnt work and instead of calling from the view's whos constraint is changed . it must be called from the upper view like this : self.view.layoutIfNeeded().
ex :
UIView.animate(withDuration: 0.1,
delay: 0.1,
options: UIViewAnimationOptions.curveEaseIn,
animations: { () -> Void in
constraintHeight.constant = 10.00
// instead of myView.layoutIfNeeded() // Swift 2
self.view.layoutIfNeeded() // Swift 3
}, completion: { (finished) -> Void in
// ....
})
The problem is, in my case i did the change and the way im using this animation is for a bottomview (a bottom bar/ banner view) that hides when scrolling a tableview down and comes up when going all the way to the top in the tableview. now that i have changed the proper code for swift 3 using self.view.layoutIfNeeded() , the tableview acts wierd, slows down, rows start appearing as fading in or is just way slow to present, when scrolling down and up the tableview's sections comes jumping or moving in slow motion, also have seem memory gone up from 80mb to 100mb . if i eliminate the line in the code, i dont get the animation, the view just appears and dissapears with the scrolling of the tableview, but... i dont get the strange behavior. i have also checked the views hierarchy to check somehow is not creating wierd 100 views replicating or something.. any hints on how can i fix this . all of this was just working fine in swift 2 using theView.layoutIfneeded() but now that the call is being madein the upper view.. omg..wierd acting
Question comes from Swift 3 UIView animation solution.
Try this to force layout changes to superview
//Force to complete all changes.
self.view.superview.layoutIfNeeded()
//Update constant
constraintHeight.constant = 10.00
UIView.animate(withDuration: 0.1,
delay: 0.1,
options: UIViewAnimationOptions.curveEaseIn,
animations: { () -> Void in
self.view.superview.layoutIfNeeded()
}, completion: { (finished) -> Void in
// ....
})
Solution to my needs . thanks all for sharing your answers!.
my view hierarchy
- root view
- uitableView
- bottomBarView
- bottomBannerView
Beacause of that hierarchy i couldnt use self.view.layoutIfNeeded() or self.bottomBarView.superview?.layoutIfneeded(), as it was calling to layout the same superview which also host the tableview and for which im triggering this function if scrolling is more than 10 and also if it less.. so its always trigerring the layoufIfneededmethod. i had to do what i thought from the beginning.
The correct way is to make a container view hosting the bottombarview and the banner view, have it constraint to bottom of the root super view and constraint the bottomBarView and bottomBannerView to IT.
View hierarchy now is .
-root view
-uitableView
-containerBottomView
-bottomBarView
-bottomBannerView
This way i can call self.bottomBarView.superview?.layoutIfNeeded() , and it wont be triggering on the root view which also host the uitableview. it correctly triggers to layout the containerBottomView.
UIView.animate(withDuration: 0.5, delay: 0.3, options: [.repeat, .curveEaseOut, .autoreverse], animations: {
// perform your animation here .
self.username.center.x += self.view.bounds.width
self.view.layoutIfNeeded()
}, completion: nil)

how to animate dependent constraints

I'm trying to animate 2 constraints(which determine position on y axis and height) of the same view at same time. The problem is, that they are dependent, by which I mean, they work together to determine the same property. The picture shows what I mean.
2 arrows depict constraints. The upper one sets the distance between upper view and bottom view, the bottom constrains sets the distance between bottom layout guide and bottom view. The bottom view slides from below the view and stops at the position you are currently seeing. The height is set with animating constraints. This is the code I'm using to make animation:
self.toBottomConstraint.constant = 53
self.toUpperConstraint.constant = 15
UIView.animateWithDuration(0.2, delay: 0, usingSpringWithDamping: 20, initialSpringVelocity: 20, options: UIViewAnimationOptions.CurveLinear, animations: {
self.view.layoutIfNeeded()
}, completion: {_ in})
The code actually works but I'm getting this error.
Unable to simultaneously satisfy constraints.
Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) ...
....
....
....
So how should I animate 2 dependent constraints?
EDIT: //upper view constraints.
// bottom view constraints. BuyButton and collection view are nested inside bottom view
Depending on how you compose your animation, there're literally infinite ways to set the constraints before and after the animation. Most importantly, you need to decide what the constraints were, before the animation, and what they will be after the animation. For example:
self.toBottomConstraint.constant = 153
self.toUpperConstraint.constant = 0
UIView.animateWithDuration(0.2, delay: 0.0, options: UIViewAnimationOptions.CurveEaseInOut, animations: { () -> Void in
self.toBottomConstraint.constant = 53
self.toUpperConstraint.constant = 15
self.view.layoutIfNeeded()
}, completion: nil)
The code above will animate the bottom view bottom up.
By the way, the console warning message for conflict constraints might be caused by other constraint you set on the bottom view apart from its top and bottom constraints, for example, you might have configured a height constraint for the bottom view, in which case, you need to update the height constraint after the animation.
You're almost there, just move the changing of the constants into the closure and call layoutIfNeeded() earlier too:
self.layoutIfNeeded()
UIView.animateWithDuration(0.2, delay: 0, usingSpringWithDamping: 20, initialSpringVelocity: 20, options: UIViewAnimationOptions.CurveLinear, animations: {
self.toBottomConstraint.constant = 53
self.toUpperConstraint.constant = 15
self.view.layoutIfNeeded()
}, completion: {_ in})
You can read more about animating constraints in another answer about constraint changes.

Difficulty allowing user interaction during UIView animation

I'm struggling to figure out how to allow user interaction with a view as it's being animated.
Here's the situation: I have a UIView cardView which holds card subviews. The cards are draggable tiles, similar to how the cards in Tinder are draggable/swipeable.
I am trying to fade out the card using animateWithDuration by animating to cardView.alpha = 0. Logically, this will also fade out all of the subviews (card objects). In this specific case, I am only targeting one card subview. However, during the animation, I am unable to drag/interact with the card.
Here is the code I'm using:
UIView.animateWithDuration(
duration,
delay: 0,
options: UIViewAnimationOptions.AllowUserInteraction,
animations: {self.cardView.alpha = 0}
) {
_ in
println("Card faded out")
card.removeFromSuperview()
}
Why doesn't this work? Any help will be appreciated. Thank you!!
I think you can find the answer in this previous post.
The interesting bit of the post is:
UIView's block animation by default blocks user interaction, and to get around it you need to pass UIViewAnimationOptionAllowUserInteraction as one of the options.
I fixed this problem by setting alpha to 0.1 instead of 0.0. I'm not sure if that will work in your case, but it shows that the event handling code thought that the view was not visible and disabled interaction even with the UIViewAnimationOptionAllowUserInteraction flag set. Oddly, setting the alpha to 0.01 did not work, so there is a threshold of visibility you have to stay above.
Swift 5
UIView.animateKeyframes(withDuration: 0.5, delay: 0, options: [.repeat, .autoreverse, .allowUserInteraction], animations: {
self.customButton.backgroundColor = .none
}, completion: nil)
The issue is with the Alpha value of 0. Alpha values of a certain proximity to Zero will remove the view from the view responder hierarchy. The fix here is to make the alpha setting to this:
self.cardView.alpha = 0.011
The view will still be invisible but not removed from the responder chain. From my testing the minimum amount is the following:
extension CGFloat {
static let minAlphaForTouchInput: CGFloat = 0.010000001
}

Resources