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()
}
}
Related
Numerous tutorials on animating AutoLayout constraints suggest to update constant property of a constraint and then call layoutIfNeeded() in animation block.
My situation is a bit tricky.
I have a view that houses 3 subviews. The height of this superview is not fixed - it is calculated as a sum of heights of its subviews.
On some event, I ask one of those 3 subviews to toggle its height (it changes between 0 and 30, i.e. I want to smoothly hide and show it).
The code is similar to this:
// In superview
subview1.isVisibleInContext = !subview1.isVisibleInContext
// In subview
class MySubview: UIView {
#IBOutlet var heightConstraint: NSLayoutConstraint!
var isVisibleInContext = false {
didSet {
updateHeight()
}
}
func toggleHeight() {
let newHeight = isVisibleInContext ? 30 : 0
layoutIfNeeded()
heightConstraint.constant = newHeight
UIView.animate(withDuration: 0.8) {
self.layoutIfNeeded()
}
}
}
Unfortunately, this does not work as I expect.
I can see the smooth change of the height of my subview, but the height of my superview is recalculated immediately after I set the new value for my subview height constraint.
I want to see the height of the superview gradually increasing/decreasing as on of its subviews grows or decreases.
Please someone point me in the right direction. Any help is appreciated.
Thanks a lot!
The animation block should be in the UIView that contains the 3 MySubviews. Inside the MySubview class you only update the height constraint's constant:
In Subview
func toggleHeight() {
let newHeight = isVisibleInContext ? 30 : 0
heightConstraint.constant = newHeight
}
Then in the UIView that contains the 3 MySubviews you animate the change:
In Superview
func toggleHeight(with subview: MySubview) {
subview.toggleHeight()
UIView.animate(withDuration: 0.8) {
self.view.layoutIfNeeded()
}
}
The first thing, that was incorrect in my approach was executing self.layoutIfNeeded(). Having investigated the issue I learned out that I had to execute it on the superivew, like this:
self.superview?.layoutIfNeeded()
That also didn't work out for a reason. The main issue in this case was that the view, which had 3 subviews inside was itself a subview of view. So to make it work I had to use the following code:
self.superview?.superview?.layoutIfNeeded()
Definitely not good that subview has to know the hierarchy of views, however it works.
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'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.
I set the constraints of the view I dragged on to the storyboard so that the view reaches over the whole screen. (I pinned it 0 from the top, left, bottom and right). When the view appears I want the view to shrink. The constant should animate from 0 to 20.
In the app, it changes the constant, but it does not animate it. Also, behind that shrunk view, everything is black although it should be white (because behind that view is the main view which is white).
override func viewDidAppear(animated: Bool) {
UIView.animateWithDuration(2.5, animations: {
self.leftPin.constant = 20.0
self.topPin.constant = 20.0
self.rightPin.constant = 20.0
self.bottomPin.constant = 20.0
// bg is my view I want to shrink
self.bg.layoutIfNeeded()
})
}
I read that in order to "apply" new constraints to the actual layout, You have to run layoutIfNeeded(). But those constraints above also work without that function. Why?
#try this
override func viewDidAppear(animated: Bool) {
self.leftPin.constant = 20.0
self.topPin.constant = 20.0
self.rightPin.constant = 20.0
self.bottomPin.constant = 20.0
UIView.animateWithDuration(2.5, animations: {
self.view.layoutIfNeeded()
})
}
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()
}
}