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()
}
Related
I'm working on a custom UIView. I got it all working properly, but my design requirements dictate that parts of the view should animate upon load.
My view is set in the following way, and I chose to animate constraints:
So I called UIView.animate() in didMoveToSuperview() like such:
override func didMoveToSuperview() {
animateArrow()
}
private func animateArrow() {
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .autoreverse, .repeat], animations: {
self.arrowLeadingConstraint.constant += 15
self.layoutIfNeeded()
}, completion: nil)
}
I'm not doing anything else. On its own, the animation only affects the leading constraint of my arrow image view. As expected, and as it should. I can verify this when I start the animation upon user interaction, as pictured below.
Now, the problem is, when called from within didMoveToSuperview(), the animation somehow affects all subviews of my custom UIView...
What am I doing wrong ?
it seems the devil is in your animateArrow() method itself, if you amend the method a little bit like e.g. this:
private func animateArrow() {
self.layoutIfNeeded()
self.arrowLeadingConstraint.constant = 31 // = 15 + 16 from your original code
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .autoreverse, .repeat], animations: {
self.layoutIfNeeded()
}, completion: nil)
}
tada, the animation will work properly as you expected.
why...?
my explanation may not be academic here but I hope it will make sense to the readers for getting a better understanding.
so, briefly, when you are dealing with constraints you are implicitly dealing with a set of predefined relationships between the view and its surroundings. that is why you cannot animate an individual constraint successfully (your original attempt) because only these relationships are animatable in this context – not the constraints.
therefore you will be able to animate the update of all relationships only after you defined the new constraint(s) for your layout – and in principle behind the scenes that could lead to animate every affected view's frame for you in one go.
you can read more about what the constraints are and how the evaluation works with Auto-Layout from Apple, if you are interested in that.
Using didMoveToSuperview might not be the best idea. Before starting the animation you need to make sure that the layout for all the views on the screen has been done, which might not always be true in didMoveToSuperview.
I would move the animation trigger inside viewDidAppear in the viewController or in didLayoutSubviews which is also in the viewcontroller.
Call the animation code here:
override func draw(_ rect: CGRect) {
super.draw(rect)
animateArrow()
}
Or
as suggested by #holex comment: perform a layout pass before:
private func animateArrow() {
self.layoutIfNeeded();
self.arrowLeadingConstraint.constant = 31;
UIView.animate(withDuration: 0.3, delay: 0, options: [.curveEaseOut, .autoreverse, .repeat]) {
self.layoutIfNeeded()
}
}
Also add an observer in your init if animation stops on pressing home:
NotificationCenter.default.addObserver(self, selector: #selector(enteredForeground(_:)), name: .UIApplicationWillEnterForeground, object: nil)
I have an animation who modify the constraint of an UIView and i need to know the size of this UIView after animate this, but before the animation begin...
storyboard
When the user scroll the UITableView I update the heightConstraint of the black UIView, I need the height after update the UIView because i need in the function viewDidLayoutSubviews to fixe the height of the Yellow UIview.
this is the code I use for animate the UIView:
self.headerIsCollapsed = true
self.heightProfilView.constant -= (self.view.bounds.height / 5)
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
self.imageUserProfil.layer.opacity = 0
})
and i need the value here :
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
heightTopBar.constant = // here i need the height of the UIView after animating this
}
So, the question is: How I can pre-calculate the height of the UIView after animating this ?
Simply use a completion block of UIView.animate()
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
self.imageUserProfil.layer.opacity = 0
}) { (finished) in
// get the view's height after animation here
}
Unless you specified duration as 0, the completion block will execute after animation sequence ends.
Try this:
self.headerIsCollapsed = true
//create this property at instance level in your viewcontroller class
self.heightBeforeAnimation = self.heightProfileView.frame.size.height
self.heightProfilView.constant -= (self.view.bounds.height / 5)
UIView.animate(withDuration: 0.3, animations: {
self.view.layoutIfNeeded()
self.imageUserProfil.layer.opacity = 0
}){ (isCompleted) in
//create this property at instance level in your viewcontroller class
self.heightAfterAnimation = self.heightProfileView.frame.size.height
}
and call whatever function and use the value of two variables containing value of height before and after animating your view.
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)
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
I am animating a view by:
#IBAction func showInfo(sender: AnyObject) {
UIView.animateWithDuration(1,
delay: 0,
usingSpringWithDamping: 0.7,
initialSpringVelocity: 0.5,
options: nil,
animations: {
self.infoContainer.frame.origin.y = CGFloat(30)
}, completion: nil
)
}
My question now is if I need to run layoutIfNeeded() afterwards?
You need to call layoutIfNeeded() when you change constraints. You don't need to call it when animating by origin
Since you are animating by setting a new frame, you don't need to call layoutIfNeeded().
My guess is that you asked this question because it's not working for you. If you setup the view with AutoLayout, and try to modify the frame directly, it won't work, you have to animate by modifying the constants in the constraints.