I'm updating an old app with an AdBannerView and when there is no ad, it slides off screen. When there is an ad it slides on the screen. Basic stuff.
Old style, I set the frame in an animation block.
New style, I have a IBOutlet to the auto-layout constraint which determines the Y position, in this case it's distance from the bottom of the superview, and modify the constant:
- (void)moveBannerOffScreen {
[UIView animateWithDuration:5 animations:^{
_addBannerDistanceFromBottomConstraint.constant = -32;
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
[UIView animateWithDuration:5 animations:^{
_addBannerDistanceFromBottomConstraint.constant = 0;
}];
bannerIsVisible = TRUE;
}
And the banner moves, exactly as expected, but no animation.
UPDATE: I re-watched WWDC 12 talk Best Practices for Mastering Auto Layout which covers animation. It discusses how to update constraints using CoreAnimation:
I've tried with the following code, but get the exact same results:
- (void)moveBannerOffScreen {
_addBannerDistanceFromBottomConstraint.constant = -32;
[UIView animateWithDuration:2 animations:^{
[self.view setNeedsLayout];
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
_addBannerDistanceFromBottomConstraint.constant = 0;
[UIView animateWithDuration:2 animations:^{
[self.view setNeedsLayout];
}];
bannerIsVisible = TRUE;
}
On a side note, I have checked numerous times and this is being executed on the main thread.
Two important notes:
You need to call layoutIfNeeded within the animation block. Apple actually recommends you call it once before the animation block to ensure that all pending layout operations have been completed
You need to call it specifically on the parent view (e.g. self.view), not the child view that has the constraints attached to it. Doing so will update all constrained views, including animating other views that might be constrained to the view that you changed the constraint of (e.g. View B is attached to the bottom of View A and you just changed View A's top offset and you want View B to animate with it)
Try this:
Objective-C
- (void)moveBannerOffScreen {
[self.view layoutIfNeeded];
[UIView animateWithDuration:5
animations:^{
self._addBannerDistanceFromBottomConstraint.constant = -32;
[self.view layoutIfNeeded]; // Called on parent view
}];
bannerIsVisible = FALSE;
}
- (void)moveBannerOnScreen {
[self.view layoutIfNeeded];
[UIView animateWithDuration:5
animations:^{
self._addBannerDistanceFromBottomConstraint.constant = 0;
[self.view layoutIfNeeded]; // Called on parent view
}];
bannerIsVisible = TRUE;
}
Swift 3
UIView.animate(withDuration: 5) {
self._addBannerDistanceFromBottomConstraint.constant = 0
self.view.layoutIfNeeded()
}
I appreciate the answer provided, but I think it would be nice to take it a bit further.
The basic block animation from the documentation
[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
// Make all constraint changes here
[containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];
but really this is a very simplistic scenario. What if I want to animate subview constraints via the updateConstraints method?
An animation block that calls the subviews updateConstraints method
[self.view layoutIfNeeded];
[self.subView setNeedsUpdateConstraints];
[self.subView updateConstraintsIfNeeded];
[UIView animateWithDuration:1.0f delay:0.0f options:UIViewAnimationOptionLayoutSubviews animations:^{
[self.view layoutIfNeeded];
} completion:nil];
The updateConstraints method is overridden in the UIView subclass and must call super at the end of the method.
- (void)updateConstraints
{
// Update some constraints
[super updateConstraints];
}
The AutoLayout Guide leaves much to be desired but it is worth reading. I myself am using this as part of a UISwitch that toggles a subview with a pair of UITextFields with a simple and subtle collapse animation (0.2 seconds long). The constraints for the subview are being handled in the UIView subclasses updateConstraints methods as described above.
Generally, you just need to update constraints and call layoutIfNeeded inside the animation block. This can be either changing the .constant property of an NSLayoutConstraint, adding remove constraints (iOS 7), or changing the .active property of constraints (iOS 8 & 9).
Sample Code:
[UIView animateWithDuration:0.3 animations:^{
// Move to right
self.leadingConstraint.active = false;
self.trailingConstraint.active = true;
// Move to bottom
self.topConstraint.active = false;
self.bottomConstraint.active = true;
// Make the animation happen
[self.view setNeedsLayout];
[self.view layoutIfNeeded];
}];
Sample Setup:
Controversy
There are some questions about whether the constraint should be changed before the animation block, or inside it (see previous answers).
The following is a Twitter conversation between Martin Pilkington who teaches iOS, and Ken Ferry who wrote Auto Layout. Ken explains that though changing constants outside of the animation block may currently work, it's not safe and they should really be change inside the animation block.
https://twitter.com/kongtomorrow/status/440627401018466305
Animation:
Sample Project
Here's a simple project showing how a view can be animated. It's using Objective C and animates the view by changing the .active property of several constraints.
https://github.com/shepting/SampleAutoLayoutAnimation
// Step 1, update your constraint
self.myOutletToConstraint.constant = 50; // New height (for example)
// Step 2, trigger animation
[UIView animateWithDuration:2.0 animations:^{
// Step 3, call layoutIfNeeded on your animated view's parent
[self.view layoutIfNeeded];
}];
Swift 4 solution
UIView.animate
Three simple steps:
Change the constraints, e.g.:
heightAnchor.constant = 50
Tell the containing view that its layout is dirty and that the autolayout should recalculate the layout:
self.view.setNeedsLayout()
In animation block tell the layout to recalculate the layout, which is equivalent of setting the frames directly (in this case the autolayout will set the frames):
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Complete simplest example:
heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
Sidenote
There is an optional 0th step - before changing the constraints you might want to call self.view.layoutIfNeeded() to make sure that the starting point for the animation is from the state with old constraints applied (in case there were some other constraints changes that should not be included in animation):
otherConstraint.constant = 30
// this will make sure that otherConstraint won't be animated but will take effect immediately
self.view.layoutIfNeeded()
heightAnchor.constant = 50
self.view.setNeedsLayout()
UIView.animate(withDuration: 0.5) {
self.view.layoutIfNeeded()
}
UIViewPropertyAnimator
Since with iOS 10 we got a new animating mechanism - UIViewPropertyAnimator, we should know that basically the same mechanism applies to it. The steps are basically the same:
heightAnchor.constant = 50
self.view.setNeedsLayout()
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
animator.startAnimation()
Since animator is an encapsulation of the animation, we can keep reference to it and call it later. However, since in the animation block we just tell the autolayout to recalculate the frames, we have to change the constraints before calling startAnimation. Therefore something like this is possible:
// prepare the animator first and keep a reference to it
let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UICubicTimingParameters(animationCurve: .linear))
animator.addAnimations {
self.view.layoutIfNeeded()
}
// at some other point in time we change the constraints and call the animator
heightAnchor.constant = 50
self.view.setNeedsLayout()
animator.startAnimation()
The order of changing constraints and starting an animator is important - if we just change the constraints and leave our animator for some later point, the next redraw cycle can invoke autolayout recalculation and the change will not be animated.
Also, remember that a single animator is non-reusable - once you run it, you cannot "rerun" it. So I guess there is not really a good reason to keep the animator around, unless we use it for controlling an interactive animation.
Swift solution:
yourConstraint.constant = 50
UIView.animate(withDuration: 1.0, animations: {
yourView.layoutIfNeeded
})
Storyboard, Code, Tips and a few Gotchas
The other answers are just fine but this one highlights a few fairly important gotchas of animating constraints using a recent example. I went through a lot of variations before I realized the following:
Make the constraints you want to target into Class variables to hold a strong reference. In Swift I used lazy variables:
lazy var centerYInflection:NSLayoutConstraint = {
let temp = self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
return temp!
}()
After some experimentation I noted that one MUST obtain the constraint from the view ABOVE (aka the superview) the two views where the constraint is defined. In the example below (both MNGStarRating and UIWebView are the two types of items I am creating a constraint between, and they are subviews within self.view).
Filter Chaining
I take advantage of Swift's filter method to separate the desired constraint that will serve as the inflection point. One could also get much more complicated but filter does a nice job here.
Animating Constraints Using Swift
Nota Bene - This example is the storyboard/code solution and assumes
one has made default constraints in the storyboard. One can then
animate the changes using code.
Assuming you create a property to filter with accurate criteria and get to a specific inflection point for your animation (of course you could also filter for an array and loop through if you need multiple constraints):
lazy var centerYInflection:NSLayoutConstraint = {
let temp = self.view.constraints.filter({ $0.firstItem is MNGStarRating }).filter ( { $0.secondItem is UIWebView }).filter({ $0.firstAttribute == .CenterY }).first
return temp!
}()
....
Sometime later...
#IBAction func toggleRatingView (sender:AnyObject){
let aPointAboveScene = -(max(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height) * 2.0)
self.view.layoutIfNeeded()
//Use any animation you want, I like the bounce in springVelocity...
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 0.75, options: [.CurveEaseOut], animations: { () -> Void in
//I use the frames to determine if the view is on-screen
if CGRectContainsRect(self.view.frame, self.ratingView.frame) {
//in frame ~ animate away
//I play a sound to give the animation some life
self.centerYInflection.constant = aPointAboveScene
self.centerYInflection.priority = UILayoutPriority(950)
} else {
//I play a different sound just to keep the user engaged
//out of frame ~ animate into scene
self.centerYInflection.constant = 0
self.centerYInflection.priority = UILayoutPriority(950)
self.view.setNeedsLayout()
self.view.layoutIfNeeded()
}) { (success) -> Void in
//do something else
}
}
}
The many wrong turns
These notes are really a set of tips that I wrote for myself. I did all the don'ts personally and painfully. Hopefully this guide can spare others.
Watch out for zPositioning. Sometimes when nothing is apparently
happening, you should hide some of the other views or use the view
debugger to locate your animated view. I've even found cases where a User Defined Runtime
Attribute was lost in a storyboard's xml and led to the animated
view being covered (while working).
Always take a minute to read the documentation (new and old), Quick
Help, and headers. Apple keeps making a lot of changes to better
manage AutoLayout constraints (see stack views). Or at least the AutoLayout Cookbook. Keep in mind that sometimes the best solutions are in the older documentation/videos.
Play around with the values in the animation and consider using
other animateWithDuration variants.
Don't hardcode specific layout values as criteria for determining
changes to other constants, instead use values that allow you to
determine the location of the view. CGRectContainsRect is one
example
If needed, don't hesitate to use the layout margins associated with
a view participating in the constraint definition
let viewMargins = self.webview.layoutMarginsGuide: is on example
Don't do work you don't have to do, all views with constraints on the
storyboard have constraints attached to the property
self.viewName.constraints
Change your priorities for any constraints to less than 1000. I set
mine to 250 (low) or 750 (high) on the storyboard; (if you try to change a 1000 priority to anything in code then the app will crash because 1000 is required)
Consider not immediately trying to use activateConstraints and
deactivateConstraints (they have their place but when just learning or if you are using a storyboard using these probably means your doing too much ~ they do have a place though as seen below)
Consider not using addConstraints / removeConstraints unless you are
really adding a new constraint in code. I found that most times I
layout the views in the storyboard with desired constraints (placing
the view offscreen), then in code, I animate the constraints previously created in the storyboard to move the view around.
I spent a lot of wasted time building up constraints with the new
NSAnchorLayout class and subclasses. These work just fine but it
took me a while to realize that all the constraints that I needed
already existed in the storyboard. If you build constraints in code
then most certainly use this method to aggregate your constraints:
Quick Sample Of Solutions to AVOID when using Storyboards
private var _nc:[NSLayoutConstraint] = []
lazy var newConstraints:[NSLayoutConstraint] = {
if !(self._nc.isEmpty) {
return self._nc
}
let viewMargins = self.webview.layoutMarginsGuide
let minimumScreenWidth = min(UIScreen.mainScreen().bounds.width,UIScreen.mainScreen().bounds.height)
let centerY = self.ratingView.centerYAnchor.constraintEqualToAnchor(self.webview.centerYAnchor)
centerY.constant = -1000.0
centerY.priority = (950)
let centerX = self.ratingView.centerXAnchor.constraintEqualToAnchor(self.webview.centerXAnchor)
centerX.priority = (950)
if let buttonConstraints = self.originalRatingViewConstraints?.filter({
($0.firstItem is UIButton || $0.secondItem is UIButton )
}) {
self._nc.appendContentsOf(buttonConstraints)
}
self._nc.append( centerY)
self._nc.append( centerX)
self._nc.append (self.ratingView.leadingAnchor.constraintEqualToAnchor(viewMargins.leadingAnchor, constant: 10.0))
self._nc.append (self.ratingView.trailingAnchor.constraintEqualToAnchor(viewMargins.trailingAnchor, constant: 10.0))
self._nc.append (self.ratingView.widthAnchor.constraintEqualToConstant((minimumScreenWidth - 20.0)))
self._nc.append (self.ratingView.heightAnchor.constraintEqualToConstant(200.0))
return self._nc
}()
If you forget one of these tips or the more simple ones such as where to add the layoutIfNeeded, most likely nothing will happen: In which case you may have a half baked solution like this:
NB - Take a moment to read the AutoLayout Section Below and the
original guide. There is a way to use these techniques to supplement
your Dynamic Animators.
UIView.animateWithDuration(1.0, delay: 0.0, usingSpringWithDamping: 0.3, initialSpringVelocity: 1.0, options: [.CurveEaseOut], animations: { () -> Void in
//
if self.starTopInflectionPoint.constant < 0 {
//-3000
//offscreen
self.starTopInflectionPoint.constant = self.navigationController?.navigationBar.bounds.height ?? 0
self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
} else {
self.starTopInflectionPoint.constant = -3000
self.changeConstraintPriority([self.starTopInflectionPoint], value: UILayoutPriority(950), forView: self.ratingView)
}
}) { (success) -> Void in
//do something else
}
}
Snippet from the AutoLayout Guide (note the second snippet is for using OS X). BTW - This is no longer in the current guide as far as I can see. The preferred techniques continue to evolve.
Animating Changes Made by Auto Layout
If you need full control over animating changes made by Auto Layout, you must make your constraint changes programmatically. The basic concept is the same for both iOS and OS X, but there are a few minor differences.
In an iOS app, your code would look something like the following:
[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
// Make all constraint changes here
[containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];
In OS X, use the following code when using layer-backed animations:
[containterView layoutSubtreeIfNeeded];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setAllowsImplicitAnimation: YES];
// Make all constraint changes here
[containerView layoutSubtreeIfNeeded];
}];
When you aren’t using layer-backed animations, you must animate the constant using the constraint’s animator:
[[constraint animator] setConstant:42];
For those who learn better visually check out this early video from Apple.
Pay Close Attention
Often in documentation there are small notes or pieces of code that lead to bigger ideas. For example attaching auto layout constraints to dynamic animators is a big idea.
Good Luck and May the Force be with you.
Working Solution 100% Swift 5.3
i have read all the answers and want to share the code and hierarchy of lines which i have used in all my applications to animate them correctly, Some solutions here are not working, you should check them on slower devices e.g iPhone 5 at this moment.
self.btnHeightConstraint.constant = 110
UIView.animate(withDuration: 0.27) { [weak self] in
self?.view.layoutIfNeeded()
}
I was trying to animate Constraints and was not really easy to found a good explanation.
What other answers are saying is totally true: you need to call [self.view layoutIfNeeded]; inside animateWithDuration: animations:. However, the other important point is to have pointers for every NSLayoutConstraint you want to animate.
I created an example in GitHub.
Working and just tested solution for Swift 3 with Xcode 8.3.3:
self.view.layoutIfNeeded()
self.calendarViewHeight.constant = 56.0
UIView.animate(withDuration: 0.5, delay: 0.0, options: UIViewAnimationOptions.curveEaseIn, animations: {
self.view.layoutIfNeeded()
}, completion: nil)
Just keep in mind that self.calendarViewHeight is a constraint referred to a customView (CalendarView). I called the .layoutIfNeeded() on self.view and NOT on self.calendarView
Hope this help.
There is an article talk about this:
http://weblog.invasivecode.com/post/42362079291/auto-layout-and-core-animation-auto-layout-was
In which, he coded like this:
- (void)handleTapFrom:(UIGestureRecognizer *)gesture {
if (_isVisible) {
_isVisible = NO;
self.topConstraint.constant = -44.; // 1
[self.navbar setNeedsUpdateConstraints]; // 2
[UIView animateWithDuration:.3 animations:^{
[self.navbar layoutIfNeeded]; // 3
}];
} else {
_isVisible = YES;
self.topConstraint.constant = 0.;
[self.navbar setNeedsUpdateConstraints];
[UIView animateWithDuration:.3 animations:^{
[self.navbar layoutIfNeeded];
}];
}
}
Hope it helps.
In the context of constraint animation, I would like to mention a specific situation where I animated a constraint immediately within a keyboard_opened notification.
Constraint defined a top space from a textfield to top of the container. Upon keyboard opening, I just divide the constant by 2.
I was unable to achieve a conistent smooth constraint animation directly within the keyboard notification. About half the times view would just jump to its new position - without animating.
It occured to me there might be some additional layouting happening as result of keyboard opening.
Adding a simple dispatch_after block with a 10ms delay made the animation run every time - no jumping.
Related
I have a gesture which action moves a UIView to point B from point A. How can I add a nice transition of the UIView moving from point A to point B. I am looking at making it slowly move from point A to point B. How would I do this? At the moment to move the item I set the frame to point B.
check this code and set frame what you want
[UIView animateWithDuration:0.75 animations:^{
button.frame = CGRectMake(10, 70, 100, 100);
}];
If you're using auto-layout (and you almost certainly are - it's the default) then #PinkeshGjr s solution is likely to have problems. With AutoLayout you can't manipulate a view's frame directly. At some point after moving the frame the view's constraints can snatch it back.
Instead, you want to create constraints that control the property/properties that you want to animate (x and y position in this case) and control-drag from the constraints into your view controller's header to create outlets.
Then you want to change the constant value on the constraints and call layoutIfNeeded inside your animation code. Something like this:
buttonXConstraint.constant = 100;
buttonYConstraint.constant = 100;
[UIView animateWithDuration:0.75 animations:^{
[self.button layoutIfNeeded];
}];
The code above assumes that you've created 2 constraint outlets and called them buttonXConstraint and buttonYConstraint.
You can achieve that by using animateKeyframesWithDuration or animateWithDuration:animations but it will be just a linear interpolation. If you want to have more control over the animation I recommend that you take a look at MTAnimation it's really a powerful and easy to use Lib for achieving beautiful animations
Cheers
Very nice helper method to use for animating view.
- (void) animateView:(UIView *)view ToFrame:(CGRect) frame withDuration:(float) duration withDelay:(float) startDelay
{
[UIView animateWithDuration:duration delay:startDelay options:0 animations:^{
view.frame = frame;
} completion:^(BOOL finished)
{
// Completion actions
}];
}
I have a small button in the top left of the screen and it is set in place with constraints. When this button is tapped I want a UIlabel to expand out of it to the right edge of the screen. The UIlabel will take up almost the width of the screen and be about 8Points in height. It contains a line of text. It will display for 2 seconds before reversing the animation so that the UILabel shrinks back into the small button. I am new to ios animation and am confused as there are many types! I need to know:
1) What type of animation technique could I use to create this effect? Code would be nice but I'd like to be pointed in the right direction of study.
2) I've read that UILabel can respond inadequately to animations. Is it problematic to animate them in the way I described?
3) Its imperative that the text within the UILabel(and the label itself!) adapts to various screen sizes via some kind of constraint assignment that works hand in hand with the animations. The size/position of the UILabel must be set via autolayout programmatically.
Thanks
UPDATE:
I added this line before the animationWithDuration method(note: (10,5, ....) is where the small button is):
_questionDisplay = [[UILabel alloc]initWithFrame:CGRectMake(10, 5, 0, 30)];
..this line within the first animation block:
_questionDisplay.frame = CGRectMake(10, 5, 600, 30);
..and this line after in the following animationWithDuration method which gets called 3.5 seconds after the first via an NSTimer:
_questionDisplay.frame = CGRectMake(10, 5, 0, 30);
Once the label shrinks it is removed from the view.
This looks good on iPhone6+ but looks wrong in 4s because the label is not being sized with constraints which is what I need.
objective-c
[UILabel animateWithDuration:1
animations:^{
yourView.transform = CGAffineTransformMakeScale(1.5, 1.5);
}
completion:^(BOOL finished) {
[UILabel animateWithDuration:1
animations:^{
yourView.transform = CGAffineTransformIdentity;
}];
}];
Swift
UILabel.animateWithDuration(1, animations: { () -> Void in
yourView.transform = CGAffineTransformMakeScale(1.5, 1.5)
}) { (finished: Bool) -> Void in
UILabel.animateWithDuration(1, animations: { () -> Void in
yourView.transform = CGAffineTransformIdentity
})}
It disappears because you are giving a size explicitly. (If you want multiple screens to size it correctly use equal width(proportional) to superview constraint instead of fixed width. After this, use the method mentioned. When your label will appear, it'll have the correct width due to proportional width constraint and the animation will make it grow and shrink back to its original size. Leave the animation blocks empty (no explicit frame changing!)
Just change the scaling factor(currently 1.2) until it suits you!
1) 2) and 3) are all handled here! The label font itself adjusts so you just need to add the animation
[UIView animateWithDuration:0.5 animations:^{
// grow the label up to 130%, using a animation of 1/2s
yourLabel.transform = CGAffineTransformMakeScale(1.2,1.2); //
} completion:^(BOOL finished) {
// When the "grow" animation is completed, go back to size 100% using another animation of 1/2s
[UIView animateWithDuration:3.5 animations:^{
yourLabel.transform = CGAffineTransformIdentity;
}];
}];
I'm simply trying to move two views upwards in the simplest manner, and I can't figure out how to make it work on iOS8 (but its works just fine on iOS7).
I've read that changes happened but I can't make it work anyway...
Here is the code i'm using :
CGRect txFrame; //the initial frame of my textview
CGRect btFrame; //the initial frame of my button
- (void)kbWillShow{
[UIView animateWithDuration:0.45 animations:^{
//Remembering the initial frames here
txFrame = _txReason.frame;
btFrame = _btSend.frame;
_lbTitleReason.alpha = 0.3;
//Animating
_txReason.frame = CGRectMake(txFrame.origin.x, txFrame.origin.y-55, txFrame.size.width, txFrame.size.height);
_btSend.frame = CGRectMake(btFrame.origin.x, btFrame.origin.y-75, btFrame.size.width, btFrame.size.height);
}completion:nil];
}
- (void)kbWillHide{
[UIView animateWithDuration:0.45 animations:^{
//Putting them back to their original positions.
_txReason.frame = txFrame;
_btSend.frame = btFrame;
_lbTitleReason.alpha = 1;
}completion:nil];
}
I've tried putting the "result" position in the completion block but I just does a very abrupt and weird movement, and after the duraton the view teleports to the intented position. (Which is better than doing an abrupt movement and ending up at the exact same position as it started).
This should be simple, what am I doing wrong?
I have come across this and the answer is to do with the layout constraints. You need to change the constant properties of the layout constraints that affect the view you want to change.
E.g. to change the width of a view with a width NSLayoutConstraint you would call constraint.constant = newWidth; then go ahead with your animation code. This is how I'm doing it and it works fine.
EDIT: your animation code is fine. You don't need to play with the centre property.
I have UIScrollView subclass. Its content is reusable - about 4 or 5 views are used to display hundreds of elements (while scrolling hidden objects reused and jumps to another position when its needed to see them)
What i need: ability to automatically scroll my scroll view to any position. For example my scroll view displays 4th, 5th and 6th element and when I tap some button it needs to scroll to 30th element. In other words I need standard behaviour of UIScrollView.
This works fine:
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
but I need some customisation. For example, change animation duration, add some code to perform on end of animation.
Obvious decision:
[UIView animateWithDuration:3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
} completion:^(BOOL finished) {
//some code
}];
but I have some actions connected to scroll event, and so now all of them are in animation block and it causes all subview's frames to animate too (thanks to few reusable elements all of them animates not how i want)
The question is: How can I make custom animation (in fact I need custom duration, actions on end and BeginFromCurrentState option) for content offset WITHOUT animating all the code, connected to scrollViewDidScroll event?
UPD:
Thanks to Andrew's answer(first part) I solved issue with animation inside scrollViewDidScroll:
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{
[UIView performWithoutAnimation:^{
[self refreshTiles];
}];
}
But scrollViewDidScroll must (for my purposes) executes every frame of animation like it was in case of
[self setContentOffset:CGPointMake(index*elementWidth, 0) animated:YES];
However, now it executes only once at start of animation.
How can I solve this?
Did you try the same approach, but with disabled animation in scrollViewDidScroll ?
On iOS 7, you could try wrapping your code in scrollViewDidScroll in
[UIView performWithoutAnimation:^{
//Your code here
}];
on previous iOS versions, you could try:
[CATransaction begin];
[CATransaction setDisableActions:YES];
//Your code here
[CATransaction commit];
Update:
Unfortunately that's where you hit the tough part of the whole thing. setContentOffset: calls the delegate just once, it's equivalent to setContentOffset:animated:NO, which again calls it just once.
setContentOffset:animated:YES calls the delegate as the animation changes the bounds of the scrollview and you want that, but you don't want the provided animation, so the only way around this that I can come up with is to gradually change the contentOffset of the scrollview, so that the animation system doesn't just jump to the final value, as is the case at the moment.
To do that you can look at keyframe animations, like so for iOS 7:
[UIView animateKeyframesWithDuration:duration delay:delay options:options animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(floorf(index/2) * elementWidth, 0)];
}];
[UIView addKeyframeWithRelativeStartTime:0.5 relativeDuration:0.5 animations:^{
[self setContentOffset:CGPointMake(index*elementWidth, 0)];
}];
} completion:^(BOOL finished) {
//Completion Block
}];
This will get you two updates and of course you could use some math and a loop to add up a lot more of these with the appropriate timings.
On previous iOS versions, you'll have to drop to CoreAnimation for keyframe animations, but it's basically the same thing with a bit different syntax.
Method 2:
You can try polling the presentationLayer of the scrollview for any changes with a timer that you start at the beginning of the animation, since unfortunately the presentationLayer's properties aren't KVO observable. Or you can use needsDisplayForKey in a subclass of the layer to get notified when the bounds change, but that'll require some work to set up and it does cause redrawing, which might affect performance.
Method 3:
Would be to dissect exactly what happens to the scrollView when animated is YES try and intercept the animation that gets set on the scrollview and change its parameters, but since this would be the most hacky, breakable due to Apple's changes and trickiest method, I won't go into it.
A nice way to do this is with the AnimationEngine library. It's a very small library: six files, with three more if you want damped spring behavior.
Behind the scenes it uses a CADisplayLink to run your animation block once every frame. You get a clean block-based syntax that's easy to use, and a bunch of interpolation and easing functions that save you time.
To animate contentOffset:
startOffset = scrollView.contentOffset;
endOffset = ..
// Constant speed looks good...
const CGFloat kTimelineAnimationSpeed = 300;
CGFloat timelineAnimationDuration = fabs(deltaToDesiredX) / kTimelineAnimationSpeed;
[INTUAnimationEngine animateWithDuration:timelineAnimationDuration
delay:0
easing:INTULinear
animations:^(CGFloat progress) {
self.videoTimelineView.contentOffset =
INTUInterpolateCGPoint(startOffset, endOffset, progress);
}
completion:^(BOOL finished) {
autoscrollEnabled = YES;
}];
Try this:
UIView.animate(withDuration: 0.6, animations: {
self.view.collectionView.contentOffset = newOffset
self.view.layoutIfNeeded()
}, completion: nil)
I try to set the background image in animation completion and basically it prevents the animation to happen.
This is the code for animation:
[UIView animateWithDuration: 0.5f animations: ^
{
//[buttonPanel setBackgroundImage: [UIImage imageNamed: imageName] forState: UIControlStateNormal];
CGFloat yOrigin = buttonFrame.origin.y - (self.bounds.size.height - buttonFrame.size.height);
yOrigin -= buttonFrame.size.height;
buttonPanel.frame = CGRectMake(buttonFrame.origin.x, yOrigin, buttonFrame.size.width, buttonFrame.size.height);
}
completion: ^(BOOL finished)
{
[UIView animateWithDuration: 0.5f animations:^
{
[buttonPanel setBackgroundImage: [UIImage imageNamed: imageName] forState: UIControlStateNormal];
[self setAlpha: 1.0f];
}];
}];
If I remove the setBackgroundImage: message it just animating to new frame position correctly. Why?
Thank you.
OK, new answer! Your comment gave me the clue I needed. You said:
Makes the button to return to initial frame for a reason. basically any operation, like calling a modal UIViewController from this UIViewController sets the button to initial Fram position
Right. Because showing a modal controller and then dismissing it causes your view to appear, and causes layout to happen. You must be using Auto Layout! Auto Layout would cause this to happen. So the problem here is that you don't understand how to change the position/size of a view (in animation or any other time) when using Auto Layout.
The rule is simple. He who lives by the constraint dies by the constraint. If you are using Auto Layout, you must not change the frame of anything. You must make all positional / size adjustments by changing a constraint.
The wonderful thing is that this is usually much easier and simpler than changing the frame! You don't have to fetch the frame, change it, and assign the frame; instead, you can usually just change a constraint's constant directly.
In this example, I animate the shrinkage of a UITextView so that the whole UITextView will be visible when the keyboard rises from the bottom of the screen (iPhone):
[UIView animateWithDuration:duration.floatValue
animations:
^{
self.bottomConstraint.constant = -r.size.height;
[self.view layoutIfNeeded];
} completion:nil];
That is how to animate a constraint: as you change the constant, you must also call layoutIfNeeded to get new layout at each instant of the animation.
So, I'm not saying that you are wrong not to use Auto Layout. It is "opt-in"; you can start using when you want to, you can use it in some nibs and not others, etc etc. But it is much more powerful than the old way of doing layout, and in many ways it leads to simpler code - once you know how to use it.
backgroundImage is not an animatable property so why are you setting it inside an animation block? Try setting it before all the animations. Just a guess, but worth trying.
EDIT: I just tried this:
[UIView animateWithDuration:0.4 animations:^{
CGRect r = self.otherButton.frame;
r.origin.y += 100;
self.otherButton.frame = r;
[self.otherButton setBackgroundImage:[UIImage imageNamed:#"car.jpg"]
forState:UIControlStateNormal];
}];
It worked fine. So I'm not able to reproduce your issue. Is there something else going on in your code that you aren't telling us about? In other words, try reducing it to a simpler case that works, and then build it back up and that way you'll discover the point at which things go wrong.
OK,
I have found this crazy issue, I had a UIViewController, Use Autolayout check and it was messing up all my animations.
Even if you have a UIViewController checked Use Autolayout it messes up all the controllers, maybe because I don't use UINavigationController, and
the segues are modal.
This was.