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)
Related
I really can't figure out what's wrong with my code. Let me explain in detail what was my aim:
I have two UIView in the same UIViewController. They're called "redSquare" and "greenSquare".
When the UIViewController is presented I want to animate the redSquare in order to move on the Y-axis till it reaches the top-border of the greenSquare.
This is how I set the xCode project:
The behavior that I got is completely the opposite and I really can't understand why and what's happening:
Any tips or explanations for this?
Okay. Part of the problem is that you're aligning the center Y... which means that you're trying to break constraints with your animation.
Another part of the problem is that you are doing your animation in viewDidLoad, which totally runs before viewWillAppear and viewDidAppear get called.
For my own animations, I usually animate a constraint.
That is, get rid of the center-Y constraint for your red box and add a new constraint putting the red box some Y distance from the bottom of the superview. Connect this new constraint to an outlet and then you can animate like this:
#IBOutlet weak var redYConstraint : NSLayoutConstraint!
override func viewDidLoad() {
super.viewDidLoad()
// This line sets the red box to be in the center Y of the green box
self.redYConstraint.constant = self.greenSquare.frame.midY
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
UIView.animate(withDuration: 3.0, delay: 2.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.redYConstraint.constant = self.greenSquare.frame.maxY
self.view.layoutIfNeeded()
}, completion: nil)
}
I want to animate a view like using the following code:
UIView.animate(withDuration: 2.7, delay: 0.1, options: .allowAnimatedContent, animations: {
self.AVCenterY.constant = 0.8
}, completion: nil)
But it happens so fast it seems like it is not animated. On the other hand, when I animate the property alpha it is animated (it takes the 2.7 seconds to change). I used 2.7 sec to make sure the problem was that I was using a small duration time.
Constraints cannot be animated at all. It is the act of layout that can be animated:
UIView.animate(withDuration: 2.7, delay: 0.1, options: .allowAnimatedContent, animations: {
self.AVCenterY.constant = 0.8
theView.superview?.layoutIfNeeded() // *
}, completion: nil)
When we animate the act of layout (or when the runtime does so), then any constraint changes are also automatically animated.
Note that what I animate is the layout of the superview of the view that is to move. I called it theView but that is just something I made up. You will need an outlet to that view so that you can get its superview, and use the name of that outlet.
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.
I am attempting to animate a tab bar to move from below the bottom of the screen to the top while simultaneously adjusting a view's height to shrink by the height of the tab bar. Essentially, I have a "hidden" tab bar that when it unhides should animate into view and the displayView should adjust for the space the tab bar now takes up.
However, the animation is jumpy for the display view. It seems that the display view animates fine, but the subviews automatically adjust their height without any animation. Any direction on fixing this would be appreciated.
I will accept aid in either objective-c or swift, as the translation is fairly easy.
//Displays tab bar with slide up animation. If animated is false, all other params are unused
func displayTabBar(animated:Bool, duration:NSTimeInterval = 0.5, delay:NSTimeInterval = 0, options:UIViewAnimationOptions = UIViewAnimationOptions.CurveLinear, completion:((Bool) -> Void)? = nil){
if(animated){
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
self.adjustTabBarDisplayed()
}, completion: completion)
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
self.adjustDisplayViewTabDisplayed()
}, completion: nil)
}
else{
self.adjustTabBarDisplayed()
self.adjustDisplayViewTabDisplayed()
}
}
//Adjusts frame of tab bar to display tab bar
private func adjustTabBarDisplayed(){
self.tabBar.frame = CGRectMake(0,UIScreen.mainScreen().bounds.height - self.tabBar.bounds.height, self.tabBar.bounds.width, self.tabBar.bounds.height)
}
//Adjusts frame of display view to match displayed tab bar
private func adjustDisplayViewTabDisplayed(){
self.displayView.frame = CGRectMake(0,0,self.displayView.bounds.width, UIScreen.mainScreen().bounds.height - self.tabBar.bounds.height)
}
When you modify a view's size, it doesn't lay out its subviews immediately. Instead, it sets a flag indicating that it needs layout. Later, after the system has finished dispatching the event that ended up calling displayTabBar, it runs the display refresh code. The display refresh code finds views that have the needs-layout flag set and tells them to lay themselves out (by sending them layoutSubviews).
Here, you are changing your display view's size inside an animation block. Therefore change to your display view's frame will be animated. But the frames of its subviews are changing outside the animation block; they're changing later during the layout phase. You need to make them change inside the animation block.
Lucky for you, that's easy. Just call self.displayView.layoutIfNeeded() inside the animation block. Also, you only need one animation block, since all of the animation parameters are identical:
func displayTabBar(animated:Bool, duration:NSTimeInterval = 0.5, delay:NSTimeInterval = 0, options:UIViewAnimationOptions = UIViewAnimationOptions.CurveLinear, completion:((Bool) -> Void)? = nil){
if(animated){
UIView.animateWithDuration(duration, delay: delay, options: options, animations: {
self.adjustTabBarDisplayed()
self.adjustDisplayViewTabDisplayed()
// ADD THIS LINE
self.displayView.layoutIfNeeded()
}, completion: completion)
}
else{
self.adjustTabBarDisplayed()
self.adjustDisplayViewTabDisplayed()
}
}
Use the below line of code in animation block
scrollView.layoutIfNeeded()