Set back to original uilayoutconstraint - zoom in animation - ios

Using a Storyboard, I have a height constraint with 2 size class (one for regular at 200pt, one for compact at 100pt).
Because I'm animating it when the view appears, the height of the element goes from 0 (initial state) -> 200pt for regular or 100pt for compact (final state).
It is a simple "zoomIn" animation.
But the thing is that because I change programmatically the constant, I'm losing the class sizes meaning when I rotate the phone, I have to set the constant to the right size instead of having Interface Builder's automatic class size.
So how would you apply an animation to an UIElement with auto-layout (and without having to create spaghetti code in viewWillLayoutSubviews, viewDidLayoutSubviews)?

Without your code, it's not clear exactly what you're doing, but here goes anyway :) First, the best way to animate when you are using autolayout is to animate the constraint changes, e.g.:
myConstraint.constant = myConstraintInitialConstant
UIView.animateWithDuration(animationSpeed) {
self.view.layoutIfNeeded()
}
The most important thing to note here is that the constraint change is outside the animation block, what you animate is layoutIfNeeded().
But you want to know what your initial constant was when the nib was loaded, yes? Then save it in viewDidLoad(), e.g.:
private var myConstraintInitialConstant: CGFloat = 65
override func viewDidLoad() {
super.viewDidLoad()
myConstraintInitialConstant = myConstraint.constant
}

Did you try to play with the layoutIfNeeded() / layoutSubviews() methods ? This will update the frame of your UIElement after added new constraints to it
I had a top constraint set to 0 and I animate it like that :
override func layoutSubviews() {
super.layoutSubviews()
// I reset my constraint's constant if already animate
if(topConst.constant > 0){
topConst.constant = 0
}
self.viewToAnimate.layoutIfNeeded()
//Animate the constraint
topConst.constant = 100
UIView.animateWithDuration(0.8, delay: 0.2, usingSpringWithDamping: 0.7, initialSpringVelocity: 0, options: .CurveEaseIn, animations: { () -> Void in
self.viewToAnimate.layoutIfNeeded()
}, completion: nil)
}
I used layoutSubviews it works fine :D

Related

Animate subview AutoLayout constraint changes

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.

UITableView Animation Bug - Visible Area Calculated Before the Animation

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()
})

UIViews not moving correctly during animation

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()
}
}

Animate constraints and frame at the same time

I want to override the perform method of an UIStoryboardSegue subclass.
Therefore I will have to animate constraints and frames of different objects using UIView.animateWithDuration.
Is it possible, to do such animations of constraints and frames in one UIView.animateWithDuration method?
Can you post an example?
Use layoutIfNeeded() to animate constraint.
view.layoutIfNeeded()
constraintToAnimate.constant = 0
UIView.animate(withDuration: 1.0, animations: { () -> Void in
self.view.layoutIfNeeded()
}

Any reasons why my UIView does not change if I set different constrains?

I have an UIView inside my normal UIView. This is my code:
override func viewDidAppear(_ animated: Bool) {
print(canvasUnder.frame.origin.y)
let getRelativePosition = view.frame.size.height * 0.25
canvasUnder.frame.origin.y = canvasUnder.frame.origin.y + getRelativePosition
self.view.layoutIfNeeded()
print(canvasUnder.frame.origin.y)
print(getRelativePosition)
}
It does however stays at the original position. This is my print:
240.0
320.0
80.0
How can this be? Thank you. edit: this is what I want: My UIView that I want to change has a height of * 0.25 of the root view. I want that my UIView is right off the boundaries of my root view, so it needs to be 25% of the root views height, followed by a constrained movement that will push my UIView down.
Edit 2: I managed to to this, this way:
override func viewDidLayoutSubviews() {
print("called")
let getRelativePosition = view.frame.size.height * 0.25
self.CanvasUnder.frame.origin.y = self.CanvasUnder.frame.origin.y + getRelativePosition
}
However this method get called each and every time something changed. I just want that my canvasUnder is just right off the screen when the view is presented. Then, whenever I want, I want to animate that UIView to pop up. I want to use this code:
let getRelativePosition = view.frame.size.height * 0.25
self.CanvasUnder.frame.origin.y = self.CanvasUnder.frame.origin.y - getRelativePosition
UIView.animate(withDuration: 1.0, animations: {
self.view.layoutIfNeeded()
})
Or putting the change of frame inside the animate function will not work. It just keeps triggering the viewDidLayoutSubviews method which will hide again my UIView.
So how can I just hide that view right under my root view, and pop it up with an slide up animation which will take 1 second?
"this is what I want: My UIView that I want to change has a height of
* 0.25 of the root view. I want that my UIView is right off the boundaries of my root view, so it needs to be 25% of the root views
height, followed by a constrained movement that will push my UIView
down."
This is exactly what auto layout and constraints are for, so you don't have to constantly be calculating sizes.
Use constraints to "pin" your view to left, right and bottom of its superview, then set it's Height Equal to Superview Height, with a multiplier (ratio) of 1:4
That will keep its height at 25% of the "root view" and will keep it "stuck" to the bottom.
No code needed :)
To animate the view in-and-out, add an IBOutlet to the Bottom constraint, and use this code...
class TestViewController: UIViewController {
#IBOutlet weak var trayBottomView: UIView!
#IBOutlet weak var trayBottomConstraint: NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// Do any additional setup after loading the view.
// hide the "tray" view
trayBottomView.isHidden = true
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
// constraints and frame sizes are fully calculated by AutoLayout here, so...
// move the tray offscreen (below the view), and "un-hide" it
self.trayBottomConstraint.constant = -self.trayBottomView.frame.height
self.trayBottomView.isHidden = false
// this first part will just put the tray view into position, below the screen
UIView.animate(withDuration: 0.01, animations: {
self.view.layoutIfNeeded()
}, completion: { (finished: Bool) in
// now, set the tray Bottom constraint to 0, so it will end up "sitting" on the bottom of the screen
self.trayBottomConstraint.constant = 0
// animate it into view - use delay to "wait a bit" before sliding the view up
// duration of 0.75 (3/4 of a second) may be too slow, just tweak as desired
UIView.animate(withDuration: 0.75, delay: 0.25, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
})
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
#IBAction func buttonTapped(_ sender: Any) {
// if the tray Bottom Constraint is Zero, that means it is visible, so
// set it to -(its own height) to position it offscreen, below the view
// otherwise, it is already offscreen, so set it to Zero to bring it back up
if self.trayBottomConstraint.constant == 0 {
self.trayBottomConstraint.constant = -self.trayBottomView.frame.height
} else {
self.trayBottomConstraint.constant = 0
}
// animate it in or out of view
// duration of 0.75 (3/4 of a second) may be too slow, just tweak as desired
UIView.animate(withDuration: 0.75, delay: 0.0, options: UIViewAnimationOptions.curveEaseInOut, animations: {
self.view.layoutIfNeeded()
})
}
}
Any reasons why my UIView does not change if I set different constraints?
You are not changing constraints. You are saying canvasUnder.frame.origin.y. That is not a change of constraint. It is a change of frame. But you cannot directly change the frame if the view is positioned by constraints! The constraints are what positions the view, not the frame.
How can this be?
Because you change the frame, and it does change just at that little moment. But by the time you see the view, the constraints have changed it back again!
When you call self.view.layoutIfNeeded() it is triggering your view to relayout. It is being moved either by auto layout, or layout code in your viewWillLayoutSubviews/viewDidLayoutSubviews functions.
Rather then updating the frame directly, either update the constraint, eg:
canvasUnderHeightConstraint.constant = view.frame.size.height * 0.25
Or, a better solution would be to use the multiplier field of the constraint to get your desired result.
NSLayoutConstraint(item: view,
attribute: .height,
relatedBy: .equal,
toItem: canvasUnder,
attribute: .height,
multiplier: 0.25,
constant: 0)

Resources