I was informed recently by meronix that use of beginAnimations is discouraged. Reading through the UIView class reference I see that this is indeed true - according to the Apple class ref:
Use of this method is discouraged in iOS 4.0 and later. You should use
the block-based animation methods to specify your animations instead.
I see that a large number of other methods - which I use frequently - are also "discouraged" which means they'll be around for iOS 6 (hopefully) but probably will be deprecated/removed eventually.
Why are these methods being discouraged?
As a side note, right now I'm using beginAnimations in all sorts of apps, most commonly to move the view up when a keyboard is shown.
//Pushes the view up if one of the table forms is selected for editing
- (void) keyboardDidShow:(NSNotification *)aNotification
{
if ([isRaised boolValue] == NO)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.25];
self.view.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
[UIView commitAnimations];
isRaised = [NSNumber numberWithBool:YES];
}
}
Not sure how to duplicate this functionality with block-based methods; a tutorial link would be nice.
They are discouraged because there is a better, cleaner alternative
In this case all a block animation does is automatically wrap your animation changes (setCenter: for example) in begin and commit calls so you dont forget. It also provides a completion block, which means you don't have to deal with delegate methods.
Apple's documentation on this is very good but as an example, to do the same animation in block form it would be
[UIView animateWithDuration:0.25 animations:^{
self.view.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
} completion:^(BOOL finished){
}];
Also ray wenderlich has a good post on block animations: link
Another way is to think about a possible implementation of block animations
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations
{
[UIView beginAnimations];
[UIView setAnimationDuration:duration];
animations();
[UIView commitAnimations];
}
Check out this method on UIView, which makes it quite simple. The trickiest part nowadays is not allowing a block to have a strong pointer to self:
//Pushes the view up if one of the table forms is selected for editing
- (void) keyboardDidShow:(NSNotification *)aNotification
{
if ([isRaised boolValue] == NO)
{
__block UIView *myView = self.view;
[UIView animateWithDuration:0.25 animations:^(){
myView.center = CGPointMake(self.view.center.x, self.view.center.y-moveAmount);
}];
isRaised = [NSNumber numberWithBool:YES];
}
}
Related
#interface ViewController
#property(weak) IBOutlet UIView *blackView;
#end
(1) First I tried to use animation block
- (IBAction) buttonHitted:(id)sender
{
[UIView beginAnimations: nil context: NULL];
[UIView setAnimationBeginsFromCurrentStatus: NO];
self.blackView.center = CGPointMake(UIScreen.mainScreen.bounds.size.width - self.blackView.center.x, self.blackView.center.y);
[UIView commitAnimations];
}
And the Animations ALWAYS begins from current status, and I can't find out why for long.
(2)Then I tried to use code block, it is the sad same.
- (IBAction) buttonHitted:(id)sender
{
[UIView animateWithDuration:2.0 delay:0.0 options:0
animations:^(void)
{
self.blackView.center = CGPointMake(UIScreen.mainScreen.bounds.size.width - self.blackView.center.x, self.blackView.center.y);
}
completion:nil];
}
//Options to 0, indicating that UIViewAnimationOptionBeginFromCurrentState not setted
I just wanna know why the animation ALWAYS begins from current status. I checked the property of the view, it is always the last value of the current animation.
I think I found out the reason behind it.
Animations for property(like UIView_ins.center and CALayer_ins.transform) could be mixed together, even you just changed one part of it, like just center.x
What's more, CAAnimation must set this property to YES, so the default CAAction for the transform must set this to YES too.
#property(getter=isAdditive) BOOL additive;
SO the new animation do not override the flight-in one, but just mixed them together.
To make sure it change UIView animation options to UIViewAnimationOptionCurveLinear
- (IBAction) buttonHitted:(id)sender
{
[UIView animateWithDuration:3.0 delay:0.0
options:UIViewAnimationOptionCurveLinear
animations:^(void)
{
self.blackView.center = CGPointMake(UIScreen.mainScreen.bounds.size.width - self.blackView.center.x, self.blackView.center.y);
}
completion:nil];
}
You will see the lovely UIView standing there for a long time and did nothing, that is the symbol of mixed in linear way :)
While using the default EaseInOut options, it looks like the problem of setAnimationBeginsFromCurrentStatus:.
I have set
[self.progressView addObserver:self forKeyPath:#"progress" options:NSKeyValueObservingOptionNew context:nil];
if I use self.progressView.progress = 0.1; to change progress, it's ok.
but if I use [self.progressView setProgress:0.1 animated:YES];, the method -observeValueForKeyPath:ofObject:change:context: can't be trigged.
what can I do now? now I can't change [self.progressView setProgress:0.1 animated:YES]; because this code in another lib.
You're probably out of luck here. You can't control the implementation of UIProgressView. File a bug with Apple if you like, but I wouldn't hold my breath, because...
Conceptually, w/r/t the MVC pattern used in iOS and OS X, this is not something you should be doing in the first place. Views should be the "observers", not the "observees". You should find another way to achieve this.
Yes, you could probably hack this into submission by swizzling the implementation of -setProgress:animated: to call -willChangeValueForKey: and -didChangeValueForKey: before and after calling the original implementation, but do yourself a favor and find another way.
I'm run into this issue too.
I'm using the following code to solve this.
CGFloat progress = 0.3;//this is just a example.
if (progress < self.progressView.progress) {
self.progressView.progress = progress;
}
else
{
[UIView animateWithDuration:0.3 animations:^{
[self.progressView setProgress:progress animated:YES];
}completion:^(BOOL finished) {
//avoid key-value observer invalid
self.progressView.progress = progress;
}];
}
This is working for me now.
Hope it could help someone.
I'm trying to build an animation around a UIButton. The UIButton has a UIImageView that contains an image that I'd like to shrink when the UIButton is held down and then when the UIButton is let go, I'd like to play a separate animation that does a bounce.
The issue I'm experiencing right now is that the 2nd part of the animation doesn't seem to play if I press down and then up very quickly. If I press and hold (wait for the first animation to finish), then let go, it seems to work fine.
Here's the relevant code:
-(void)pressedDown
{
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.heartPart.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1);
} completion:nil];
}
-(void)pressedUp
{
[UIView animateWithDuration:0.8
delay:0.0
usingSpringWithDamping:20
initialSpringVelocity:200
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.heartPart.layer.transform = CATransform3DIdentity;
}
completion:nil];
}];
}
In my ViewDidLoad I add the following:
[self.heartButton addTarget:self
action:#selector(pressedUp)
forControlEvents:UIControlEventTouchUpInside];
[self.heartButton addTarget:self
action:#selector(PressedDown)
forControlEvents:UIControlEventTouchDown];
Any idea how I can get the two animations to sequence and not interrupt each other even on a quick press?
Here's a potential plan of action. Keep two global variables, BOOL buttonPressed and NSDate *buttonPressDate. When the button is pressed, you should set buttonPressed to true and set buttonPressDate to the current date. Now in the touch up method, you would set buttonPressed to false and check whether the time interval between buttonPressDate and the current date is greater than the duration of the animation. If it is, then run the touch up animation; if not, return. In the completion block of the touch down method, you would check if the button was still pressed. If it is, then do nothing; if it's not pressed anymore, then run the touch up animation.
The effect you'll get using this approach should be the following: if you tap the button quickly, it will run the full 0.2-second shrink animation and then run the enlarge animation in sequence.
Now, if you don't want the touch down animation to run in the case of a quick touch, you should probably delay the first animation and check if the button is still pressed when you start it. If it was a quick touch, you would run a modified animation that covered both the touch down and touch up phases.
You could try out the following :-
//Set only one target with TouchUpInside.
[self.heartButton addTarget:self
action:#selector(pressedUp)
forControlEvents:UIControlEventTouchUpInside];
-(void)pressedDown
{
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
self.heartPart.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1);
}
completion:
[self pressedUp];
//Or
[self performSelector:#selector(pressedUp) withObject:self afterDelay:0.5];
];
}
-(void)pressedUp
{
[UIView animateWithDuration:0.8
delay:0.0
usingSpringWithDamping:20
initialSpringVelocity:200
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
self.heartPart.layer.transform = CATransform3DIdentity;
}
completion:nil];
}];
}
This way as first animation is completed then next animation will start But my concern is that it won't look appropriate and it would be a bit long animation to show to a user. Rest it's upto your app design and what all it is going to achieve with it.
I assume you are familiar with the concept of operations. If not then NSOperations allow you to keep your code modular and enable you to set the order of execution.
You could create a custom subclass of NSOperation and run animation within its execution block, then add operations with animations on queue each time user interacts with the button.
Apart from documentation from Apple, there is a great example of how to subclass NSOperation available on Github:
https://github.com/robertmryan/AFHTTPSessionOperation/blob/master/Source/AsynchronousOperation.m
Based on that "blueprint" for your custom operations, it's very trivial to achieve what you want, i.e.:
#interface PressDownAnimationOperation : AsynchronousOperation
- (instancetype)initWithView:(UIView *)view;
#end
#implementation PressDownAnimationOperation {
UIView *_view;
}
- (instancetype)initWithView:(UIView *)view {
self = [super init];
if(self) {
_view = view;
}
return self;
}
- (void)main {
// dispatch UIKit related stuff on main thread
dispatch_async(dispatch_get_main_queue(), ^{
// animate
[UIView animateWithDuration:0.2 delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
_view.layer.transform = CATransform3DMakeScale(0.8, 0.8, 1);
} completion:^(BOOL finished){
// mark operation as finished
[self completeOperation];
}];
});
}
#end
Now in order to run animations you need to create NSOperationQueue. You can keep it within your view controller, i.e.:
#implementation ViewController {
NSOperationQueue *_animationQueue;
}
- (void)viewDidLoad {
[super viewDidLoad];
_animationQueue = [[NSOperationQueue alloc] init];
// limit queue to run single operation at a time
_animationQueue.maxOperationCount = 1;
}
#end
Now when you want to chain animations, you simply create new operation, add it to queue and that's it, i.e.:
- (void)pressedDown {
NSOperation *animationOperation = [[PressDownAnimationOperation alloc] init];
[_animationQueue addOperation:animationOperation];
}
If user taps too fast and you notice that your operation queue is clogged with animations, you can simply cancel all animations before adding new one, i.e.:
[_animationQueue cancelAllOperations];
I had a UIView block-based animation that looks like this:
[UIView animateWithDuration:0.5
//sequential delays
delay:3.0 * (float)(index/25)
options:UIViewAnimationOptionCurveEaseOut
animations:^{
// set myself as delegate
[UIView setAnimationDelegate:self];
// when animation stared, play some sound
[UIView setAnimationWillStartSelector:#selector(playDrawCardSound)];
// some animation results here
}
completion:^(BOOL finished){
// this log will never be printed out.
NSLog(#"finished");
}];
So, the problem is that all animations are fine, but the completion handler never got called.
Also, the reason that I am using delegate is because animations block will not be delayed as animations. That being said, all the sounds are played at once (not like animations which have delays between them).
Anybody know how to let code in animations block actually get delayed? Thanks!
You shouldn't use setAnimationDelegate or setAnimationWillStartSelector because document says Use of this method is discouraged in iOS 4.0 and later. If you are using the block-based animation methods, you can include your delegate’s start code directly inside your block.
Also setAnimationDelegate and setAnimationWillStartSelector must be called between calls to the beginAnimations:context: and commitAnimations methods which are discouraged in iOS 4.0 and later.
It may not precise but I think it may help.
NSTimeInterval delay = 3.0 * (float)(index/25) ;
[UIView animateWithDuration:0.5
//sequential delays
delay:delay
options:UIViewAnimationOptionCurveEaseOut
animations:^{
[self performSelector:#selector(playDrawCardSound) withObject:nil afterDelay:delay] ;
// some animation results here
}
completion:^(BOOL finished){
// this log will never be printed out.
NSLog(#"finished");
}];
I think you should not call delay methods in UIViewAnimation, you can just try this:
NSTimeInterval delayInterval = 3.0 * (float)(index/25);
[self performSelector:#selector(playDrawCardSound) withObject:nil afterDelay:delayInterval];
[UIView animateWithDuration:0.5
//sequential delays
delay:delayInterval
options:UIViewAnimationOptionCurveEaseOut
animations:^{
// some animation results,
//to do logic things in finish block or delay method above will be better
}
completion:^(BOOL finished){
// this log will never be printed out.
NSLog(#"finished");
}];
Try performSelector with param:
[self performSelector:#selector(doDelayTast:) withObject:[NSDate date] afterDelay:4];
- (void)doDelayTast:(id)obj{
NSDate *preDate = obj;
NSLog(#"Delay timeinterval:%f", [[NSDate date] timeIntervalSinceDate:preDate]);
}
The result:
Delay timeinterval:4.066990
Note that, 4s is the minimum time before the message is sent, but this can be ignored in most time.
Custom transitions work easily with standard containers and while presenting modal view controllers. But what about using custom transitions with a totally custom container?
I'd like to use the UIViewControllerContextTransitioning protocol with my custom container and take advantage of transitioning and interactive transitions.
In the comment into the header file of UIViewControllerContextTransitioning I read:
// The UIViewControllerContextTransitioning protocol can be adopted by custom
// container controllers. It is purposely general to cover more complex
// transitions than the system currently supports.
but I can't understand how to create a Context Transitioning and start all the custom transition process.
This is very well possible!
have a look at this SO Answer.
You just have to create a viewController conforming to the UIViewControllerContextTransitioning protocol and use the ViewController Containment API (addChildViewController:, willMoveToParentViewController:, and so on).
When you want to start the custom transition, simply call the [animateTransition] Method on your transitionController. From that point on, it's the same as with the Apple-provided API.
Make your custom container fit in with the Transitioning API.
In fact, all the new protocols introduced in iOS7 (UIViewControllerContextTransitioning and so on) are not really needed to implement your own fully custom containerViewController. Its just providing a neat class-relationship and method pool. But you could write that all by yourself and would not need to use any private API.
ViewController Containment (introduced in iOS5) is all you need for this job.
I just tried gave it a try and its even working with interactive transitions.
If you need some sample code please let me know and i'll provide some :)
EDIT
By the way, this is how Facebook and Instagram for iOS is creating their navigation concept. You can scroll away the NavigationBar in these Apps and when a new ViewController is "pushed" the NavigationBar seems to have been reset during the pushing.
Ironically, i asked the exact same question a month ago here on StackOverflow :D
My Question
And I just realized i answered it myself :)
Have a nice day !
In fact, it says so but you are incomplete:
The UIViewControllerContextTransitioning protocol can be adopted by
custom container controllers. It is purposely general to cover
more complex transitions than the system currently supports. For
now, navigation push/pops and UIViewController present/dismiss
transitions can be customized.
That means you can trigger your VC's transitioningDelegate by calling presentViewController:animated:completion: method or you can implement navigationController:animationControllerForOperation:fromViewController:toViewController: in your UINavigationControllerDelegate class and return an object that conforms UIViewControllerAnimatedTransitioning protocol.
To sum up, there are only two ways to provide custom transition. Both requires presenting/pushing a VC while we add subviews to create custom container view controllers.
Probably, Apple will do what they said in the first lines of their statement in header file in the future, but for now what you want to do seems impossible.
From docs UIViewControllerContextTransitioning
Do not adopt this protocol in your own classes, nor should you
directly create objects that adopt this protocol.
Please look at MPFoldTransition, it worked for me.
You can also use the following Category, you can create your own transition using this.
#import <UIKit/UIKit.h>
#interface UIView (Animation)
- (void) moveTo:(CGPoint)destination duration:(float)secs option:(UIViewAnimationOptions)option;
- (void) downUnder:(float)secs option:(UIViewAnimationOptions)option;
- (void) addSubviewWithZoomInAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option;
- (void) removeWithZoomOutAnimation:(float)secs option:(UIViewAnimationOptions)option;
- (void) addSubviewWithFadeAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option;
- (void) removeWithSinkAnimation:(int)steps;
- (void) removeWithSinkAnimationRotateTimer:(NSTimer*) timer;
#end
Implementation :
#import "UIView+Animation.h"
#implementation UIView (Animation)
- (void) moveTo:(CGPoint)destination duration:(float)secs option:(UIViewAnimationOptions)option
{
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
self.frame = CGRectMake(destination.x,destination.y, self.frame.size.width, self.frame.size.height);
}
completion:nil];
}
- (void) downUnder:(float)secs option:(UIViewAnimationOptions)option
{
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
self.transform = CGAffineTransformRotate(self.transform, M_PI);
self.alpha = self.alpha - 0.5;
}
completion:nil];
}
- (void) addSubviewWithZoomInAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option
{
// first reduce the view to 1/100th of its original dimension
CGAffineTransform trans = CGAffineTransformScale(view.transform, 0.01, 0.01);
view.transform = trans; // do it instantly, no animation
[self addSubview:view];
// now return the view to normal dimension, animating this tranformation
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
view.transform = CGAffineTransformScale(view.transform, 100.0, 100.0);
}
completion:nil];
}
- (void) removeWithZoomOutAnimation:(float)secs option:(UIViewAnimationOptions)option
{
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{
self.transform = CGAffineTransformScale(self.transform, 0.1, 0.1);
}
completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}
// add with a fade-in effect
- (void) addSubviewWithFadeAnimation:(UIView*)view duration:(float)secs option:(UIViewAnimationOptions)option
{
view.alpha = 0.0; // make the view transparent
[self addSubview:view]; // add it
[UIView animateWithDuration:secs delay:0.0 options:option
animations:^{view.alpha = 1.0;}
completion:nil]; // animate the return to visible
}
// remove self making it "drain" from the sink!
- (void) removeWithSinkAnimation:(int)steps
{
//NSTimer *timer;
if (steps > 0 && steps < 100) // just to avoid too much steps
self.tag = steps;
else
self.tag = 50;
[NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(removeWithSinkAnimationRotateTimer:) userInfo:nil repeats:YES];
}
- (void) removeWithSinkAnimationRotateTimer:(NSTimer*) timer
{
CGAffineTransform trans = CGAffineTransformRotate(CGAffineTransformScale(self.transform, 0.9, 0.9),0.314);
self.transform = trans;
self.alpha = self.alpha * 0.98;
self.tag = self.tag - 1;
if (self.tag <= 0)
{
[timer invalidate];
[self removeFromSuperview];
}
}
#end
I hope it solves your problem. enjoy.:)