Animate NSLayoutConstraint & view simultaneously in separate blocks - ios

I have two relatively simple animations. One is animating the top constraint of a UIButton so that it slides up. The other is animating the background color on a UIView.
self.buttonAnimationConstant.constant = 0
self.view.layoutIfNeeded() // ensure the constraint is at 0
self.buttonAnimationConstraint.constant = 100
UIView.animateWithDuration(0.25, delay: 0.0) {
self.view.layoutIfNeeded()
}
and my color animation:
UIView.animateWithDuration(0.25, delay: 0.0) {
self.colorView.backgroundColor = UIColor.purpleColor()
}
If I try executing them at the same time, then the button will animate up but it cancels the color animation, presumably because of view.layoutIfNeeded. Note that these two animations are in separate places so they can't be joined into one block (one sits at the view controller level, the other embedded inside a custom view but both within the same view controller). How can I animate both a constraint and a view property such that one doesn't cancel the other?
Essentially the issue is how do you go about animating both a constraint and a view that sit in the same view hierarchy?

Animation blocks do not cancel each other. The issue here is this line:
self.view.layoutIfNeeded() // ensure the constraint is at 0
Calling layoutIfNeeded outside an animation block will cancel your animations (because they set a new value for the values you animate) and there is not much you can do to prevent that. What you could do is making sure that no layoutIfNeeded is called outside an animation block while performing an animation. Of course this does not happen if the views are not in the same view hierarchy.

Related

First time animating is glitchy

I am animating a view gradientView in/out using the following:
func hideOrShowGradientView(hide: Bool) {
UIView.animate(withDuration: 0.4, animations: {
self.gradientView.isHidden = hide
})
}
This works well, but on the first time, there is no animation. It just appears. On the second and third time it works wonderfully. I've tried calling the animate block on the main thread but no luck there. Why is this animation failing to occur on the first and only first time around? Should I be using another animation method?
Have you tried calling self.view.layoutIfNeeded(). It forces the view and it's subviews to complete any of it's pending animations immediately, which might be interfering with your animation. You can use it like this:
func hideOrShowGradientView(hide: Bool) {
self.view.layoutIfNeeded()
UIView.animate(withDuration: 0.4, animations: {
self.gradientView.isHidden = hide
self.view.layoutIfNeeded()
})
}
Apple recommends calling layoutIfNeeded() twice, once before the animation block which forces a redraw on the view and it's subviews, and it completes any pending animations on the view without waiting for the next update cycle, and call it the second time inside the animation block to make sure that the animation changes will be applied immediately.
I had too much going on the main thread in the init method, which was being called only on the first time the view was being presented. Moved unnecessary initialization into view did appear

Changing the constant of a NSLayoutConstraint

Background
I'm trying to do a fairly standard NSLayoutConstraint constant update, with both an animated and non-animated option. To give you some background, I have a UIView subclass called ProgressView. Within the ProgressView, there is the Progress Bar UIView. The Progress Bar UIView is connected as a referencing outlet progressBar to the ProgressView class. Here's how the fairly simple hierarchy looks within the Storyboard:
As you might have guessed, I'm building a custom progress bar. The ProgressView UIView is the "track," and the Progress Bar UIView is the "fill." The plan is to use the trailing constraint of the Progress Bar UIView to make changes to the progress.
Existing configuration
The constraints for the Progress Bar UIView are set up to be flush with the superview (Progress View):
The trailing constraint of the Progress Bar UIView is connected to the ProgressView class as a strong outlet: #IBOutlet var trailingConstraint: NSLayoutConstraint!.
Within the ProgressView class, there is a setProgress function. This function is called from the ViewController which contains the ProgressView. Here's the function with comments explaining how it works:
public func setProgress(_ progress: Double, animationDuration: Double) {
// Pre-conditions
guard progress >= 0.0 && progress <= 1.0 && animationDuration >= 0.0 else {
return
}
// Calculate the new constraint constant based on the progress
let newConstant = CGFloat(1 - progress) * self.bounds.size.width
// If the update should be made without animation, just make the change and return
guard animationDuration > 0.0 else {
trailingConstraint.constant = newConstant
return
}
// Set the constraint first
self.trailingConstraint.constant = newConstant
// Animate!
UIView.animate(withDuration: animationDuration) {
self.layoutIfNeeded()
}
}
The problem
As you can see, this function can update the progress with and without animation. However, when called from the ViewController, neither works. The trailing constraint constant appears to remain 0, and the progress bar fills the entire track.
Attempted solutions
I have tried calling layoutIfNeeded() and setNeedsLayout() in every position and configuration where it makes sense (on both self [where self is the ProgressView] and on progressBar). I have tried putting the code explicitly on the main thread with DispatchQueue.main.async. As a ground truth test, I tried connecting the trailingConstraint outlet to the ViewController itself and updating the constant from the viewDidAppear() and viewDidLayoutSubviews() methods. Nothing works.
A possible hint in the right direction
Here's the weird part. The progress bar appears filled on the screen, but when I debug the view hierarchy, the progress bar looks correct. The trailingConstraint constant seems to have been set correctly.
I was testing on an iOS 10 device and running Xcode 8.3.3. This issue seems really odd to me, and I'm not quite sure what to do from here. Thanks for your help.
I faced the same problem.My problem was solved by changing constant in animation .Try this out:
UIView.animate(withDuration: 0.2) {
self.trailingConstraint.constant = newConstant
self.layoutIfNeeded()
}

UIStackView subview contents visible during hiding animation

I am using a UIStackView for my layout. In that stack view, when I press a button I want to hide one of the subviews. That subview contains a couple buttons and a label. My issue is that during the hide animation, the buttons and label are visible until the vertical space from the subview is fully animated away.
Is there something I can do so that when I call subview.isHidden = true, the subview`s contents hide immediately at the beginning of the animation instead of at the very end of the animation?
use a custom stackView class. Use IBOutlets in the class to reference the buttons/text and write a function that hides your outlets when self.isHidden = true. Let me know if you need more explanation.
Besides hiding the buttons and content view with an animation you could try changing the background color from clear on the views inside the stackview to the same color as the background on your view. This still might not look great but it would be better.
Obviously animation would be something like the code below but give background color on your content views in the stackview a shot.
UIView.animate(withDuration: 0.1, animations: {
//yourContentHoldingView.alpha = 0
})

UIStackView moves it's arranged subviews to top left when all subviews are hidden within an animation block

If I try to animate the hiding all the subviews of a stackview, I can see them moving towards the top left corner. On showing, they are animated coming from top left to their proper space.
If I hide only a subset of the arranged views, they are animated as expected.
My current workaround is to keep an invisible subview in the stack, but this is super wonky.
I am hiding via
UIView.animate(withDuration: 0.5) {
self.someStack.arrangedSubviews.forEach { $0.isHidden = !$0.isHidden
}
Try adding an additional empty view (width/height 0) into your stack view. This fixed the issue for me.
I faced a very similar problem and after a few hours of back and forth, I found that calling self.view.layoutIfNeeded() fixed the issue.
My hierarchy:
- UIView
- UIStackView
- UIStackView(1)
- UIButton
- UIButton
- UIStackView(2)
- UITextField
- UIButton
- UIActivityIndicatorView
The root UIView animates from the bottom when the keyboard appears based on a call to UITextField.becomeFirstResponder() in the (2)UIStackView. By default, every subview of (2)UIStackView is hidden. Based on a UISegmentedControl change, the app calls UITextField.becomeFirstResponder() and hides (1)UIStackView and shows (2)UIStackView. If I don't call self.view.layoutIfNeeded() after stackView.subviews.forEach { $0.isHidden = false } to show the subviews of (2)UIStackView I see them animating from the top left of the device.
I don't know if this might help you, but it might be a starting point to investigate.

Video in AVPlayer not changing size at same rate as player itself

I'm having an odd problem with animating an AVPlayer. I'm changing the size of a UIView using Layout Constraints and animating the change with a layoutIfNeeded() in an animation block. Every subview in the animated UIView animates properly, including the AVPlayer itself, but the video inside the player does not animate with the same animation curve.
The AVPlayer is embedded in a container view through an AVPlayerController. The container is a subview of the view being animated.
A video of the animation shows the problem pretty clearly: http://goo.gl/DGq8rO
I know the AVPlayer is changing size on the correct animation curve because you can see the top of the player control bar animating at the correct rate, so this has to be some issue with the video itself. I'm not too well versed in they player side of AVKit/AVFoundation, so any guidance would be appreciated.
This is the function that the animation is called from:
func updateSize(duration: NSTimeInterval, _ dampening: CGFloat) {
heightConstraint.constant = actualHeight
if duration > 0 {
UIView.animateWithDuration(duration, delay: 0.0, usingSpringWithDamping: dampening, initialSpringVelocity: 0, options: nil, animations: {
self.superview!.layoutIfNeeded()
}, completion: nil)
} else {
self.superview?.layoutIfNeeded()
}
}
I've tested it with using the basic animateWithDuration function (the one without usingSpringWithDampening), and it works properly. This is more of a workaround than a fix, though. I'm really looking for some way to use usingSpringWithDempening animations, though.
I can reproduce it when I animate size and position of containerView's superview simultaneously. If the containerView is a normal view instead of a AVPlayer view, everything works fine.
My suggestion is to use animateWithDuration:animations: instead of using usingSpringWithDamping. I think there is a CASpringAnimation used behind it, causing one of the sublayers's height to always animate from the original value to new value with a different duration. You can also animate its size firstly with a normal animation and then its position with spring animation.

Resources