I'm trying to have a simple slide-down-from-top animation using auto layout (with the help of SnapKit).
Below is the code I use to generate a view, set its constraints and force a reload (thus display) of said view. In the same code cope I change the top constraint to a different value and call layoutIfNeeded in an animation block.
What happens instead though is that the view is displayed at the intended position without any animations whatsoever.
Here is the code called in a method after a short delay (for other purposes):
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(4), execute: {
self.view.addSubview(view)
view.backgroundColor = .black
view.snp.makeConstraints { (make) -> Void in
make.left.right.equalToSuperview()
make.height.equalTo(200)
make.bottom.equalTo(self.view.snp.top) // Pin view to top to prepare slide-down animation
}
view.setNeedsLayout()
view.layoutIfNeeded()
view.snp.updateConstraints { (make) -> Void in
make.bottom.equalTo(self.view.snp.top).offset(200)
}
view.setNeedsLayout()
UIView.animate(withDuration: 5, animations: {
view.layoutIfNeeded()
})
})
Thanks to In Swift, how do I animate a UIView with auto-layout like a page sliding in? I found the culprit.
Although I don't 100% understand why this is, but layoutIfNeeded needs to be called from the superview, not the view itself.
So calling self.view.layoutIfNeeded() is the correct way. Note that view is the currently being added UIView, whereas self.view is the superview, where view is being added to.
Related
I have a simple UI (very similar to Uber) where the user can scroll a table view on top of the main content.
The UI and the corresponding UITableView animation bug can be displayed as:
Here is what is happening:
User taps on the table view and the table view is expanded. This is done via adding the layout constraint that makes sure that tableView.top = topMenu.bottom. The constraint that gets removed is tableView.height = 30. Everything looks good.
User then scrolls down a certain amount (55+ pixels) and the constraints are reverted back to their original states. This happens inside an animation block so that the flow looks smooth.
The bug occurs here. As far as I understand, the tableView's visible area is calculated of how it will look like after the animation ends. However, this calculation happens before the animation. Therefore, during the animation only 1-2 cells are displayed on the table view; causing the bug.
I can have a workaround here by temporarily setting the tableView's height to a large value and only setting it back to a small value after the animation ends. However, that doesn't work because the safe area on iPhoneX gets covered by the tableView.
Relevant code is here:
private func animateTheChange() {
UIView.animate(withDuration: 0.8, animations: {
self.view.setNeedsUpdateConstraints()
self.view.layoutIfNeeded()
})
}
override func updateViewConstraints() {
self.touchConstraints()
super.updateViewConstraints()
}
private func touchConstraints() {
if self.collapsed {
self.view.addConstraint(self.collapsedConstraint)
self.view.removeConstraint(self.expandedConstraint)
if UserHardware.IS_IPHONE_X {
self.bottomConstraint.constant = 0
}
}
else { // Expand
self.view.addConstraint(self.expandedConstraint)
self.view.removeConstraint(self.collapsedConstraint)
if UserHardware.IS_IPHONE_X {
self.bottomConstraint.constant = 34
}
}
}
Relevant Stackoverflow questions (that help but don't solve the issue):
UITableView frame height animation glitch
Dynamic UITableView height
UITableView frame change animation issue
One option...
Embed your tableView inside a "containing" UIView
Constrain the tableView to Top and Bottom of the containing view
Constrain the containing view Bottom to the Safe Area Bottom
Constrain the containing view Top to the Bottom of topMenu with a Priority of 250 (default low), and connect it to #IBOutlet var tableContainerTop: NSLayoutConstraint!
Constrain the Height of the containing view to 30 with a Priority of 750 (default high), and connect it to #IBOutlet var tableContainerHeight: NSLayoutConstraint!
When you want to "expand" or "collapse" your tableView, change the priorities of the containing view's constraints.
UIView.animate(withDuration: 0.8, animations: {
if self.isExpanded {
self.tableContainerHeight.priority = .defaultHigh
self.tableContainerTop.priority = .defaultLow
} else {
self.tableContainerHeight.priority = .defaultLow
self.tableContainerTop.priority = .defaultHigh
}
self.view.layoutIfNeeded()
})
I have a viewcontroller with 3 UIViews that are stacked on top of each other. By stacked I mean at the bottom of one view, the next view begins. I have placed vertical constraints between each view with constant = 0. When the application begins, in viewDidLoad, I'm adding 500 to the vertical constraint between the two top views, so the bottom two views are pushed down below by doing:
billViewBottomConstraint.constant = 500
I then call the following function to animate the two bottom views moving back up, ending right below the top view:
func animate()
{
self.billViewBottomConstraint.constant = 0
UIView.animate(withDuration: 2.0) {
self.view.layoutIfNeeded()
}
}
The views certainly animate to the right position, but not the way I want. It looks like before the views animate, they are expanded outwards and when the animation is called they contract up and inwards towards the right position.
Inside viewDidLoad, the layout is not ready to be animated. You should wait at least until viewDidLayoutSubviews to properly animate constraints. Check with a boolean to make sure it runs only for the first time.
fileprivate var firstLayoutSubviewsTime = true
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if firstLayoutSubviewsTime {
firstLayoutSubviewsTime = false
animate()
}
}
You should call view.layoutIfNeeded() before the animation block to make sure all views are loaded properly.
You should also modify the constant inside the animation block, not before it.
func animate() {
self.view.layoutIfNeeded()
UIView.animate(withDuration: 2.0) {
self.billViewBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
}
i've created a view with an header and a container... inside the container there is a page view controller
the header resize based on the container, throught an iboutlet constraint
if there is a table view as page in the container then the header resize based on the scroll offset, without animation
if i scroll between pages i want the header to be resized, so i've created a delegate method that listen the end of the scroll animation and then resize the header
but if i animate this resizing with this code
UIView.animate(withDuration: 0.5,animations: {
self.imageViewHeightConstraint.constant = ct
self.view.layoutIfNeeded()
}, completion: {
_ in
if let handler = handler {
handler()
}
})
then the pages are "reloaded" after the resizing it's complete
this is a video of the animation ( the red background is the background of the page view controller that i've edited for debug reason)
Video of the Wrong Animation
Please set constant outside the animation block like this,
self.imageViewHeightConstraint.constant = ct
UIView.animate(withDuration: 0.5,animations: {
self.view.layoutIfNeeded()
}, completion: {_ in
UIView.commitAnimations()
if let handler = handler {
handler()
}
})
I'm facing a problem : I have a very simple custom UIView which groups one UITextField and one UILabel.
The UILabel is on the UITextField, and I want that the UILabel go up to 40px when user begins to edit the UITextField.
I'm using constraints to set the textfield's frame and the label's frame.
So I just set the delegate in my custom view with UITextFieldDelegate protocol, and implement the delegate method : textFieldDidBeginEditing, as bellow :
I'm just animating the constraint (center Y) of my label in the delegate method.
func textFieldDidBeginEditing(textField: UITextField) {
self.centerVerticallyLabelConstraint.constant = -40
UIView.animateWithDuration(0.5) {
self.textFieldLabel.layoutIfNeeded()
}
}
What happens is that the UILabel goes up correctly, but it goes up instantly. It ignores the duration of 0.5 in my animation. I don't know why :
I'm not animating something else at the same time, I only have this animation.
I haven't disable animations for UIView.
The delegate is set to my UITextfield.
Any idea?
Try calling layoutIfNeeded on your view instead of textview.
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Hope this helps.
need to call self.view.layoutIfNeeded() not self.textFieldLabel.layoutIfNeeded()
Thanks
Try Below code to execute:
func textFieldDidBeginEditing(textField: UITextField) {
UIView.animate(withDuration: 0.5) {
self.textFieldLabel.frame.origin.y -= 40
}
}
For me, the issue seems to be because I was already on the main thread & I called the animation block inside a GCD main.async call.
ie I had
DispatchQueue.main.async {
<# update the constraint s#>
self.animatedViewsParent.setNeedsLayout()
animatedView.setNeedsLayout()
UIView.animate(withDuration: 2.0) {
self.animatedViewsParent.layoutIfNeeded()
}
}
The view animates moves but does not animate gradually over 2.0 seconds. Removing the outer DispatchQueue.main.async {} solves the problem.
I have two subViews in ViewController that located one after another (bottom of first connected to the top of second)
First view is changing its height animated (example below), so I expected that second view will descend with animation too, but its not..
How to make it all animated?
Animation block for first view
func animate(){
layoutIfNeeded()
UIView.animateWithDuration(1){
self.labelHeight.constant = 70 // this is constraint
self.layoutIfNeeded()
}
}
Constraints between sibling views are added to their shared super view so you should call layoutIfNeeded() on it. For example:
func animate(){
self.superview?.layoutIfNeeded()
UIView.animateWithDuration(1){
self.labelHeight.constant = 70 // this is constraint
self.superview?.layoutIfNeeded()
}
}