[UIView beginAnimations:nil context:NULL]; // animate the following:
gearKnob.center = startedAtPoint;
[UIView setAnimationDuration:0.3];
[UIView commitAnimations];
This is animation, which moves UIImageView (gearKnob) from point to point. The problem is that when object is moving, I need to change background accordingly, so I need to track every UIImageView position when it is moving. How can I track its position? Is there any method or delegate to do that?
If you really have to, here is an efficient way to do it.
The trick is to use a CADisplayLink timer and read the animated property from layer.presentationLayer.
#interface MyViewController ()
...
#property(nonatomic, strong) CADisplayLink *displayLink;
...
#end
#implementation MyViewController {
- (void)animateGearKnob {
// invalidate any pending timer
[self.displayLink invalidate];
// create a timer that's synchronized with the refresh rate of the display
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(updateDuringAnimation)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
// launch the animation
[UIView animateWithDuration:0.3 delay:0 options:0 animations:^{
gearKnob.center = startedAtPoint;
} completion:^(BOOL finished) {
// terminate the timer when the animation is complete
[self.displayLink invalidate];
self.displayLink = nil;
}];
}
- (void)updateDuringAnimation {
// Do something with gearKnob.layer.presentationLayer.center
}
}
As David wrote, you are probably better off running a second animation in parallel.
If you need to pseudo-animate something using a timer, you may want to try out CPAccelerationTimer, which allows you to set a Bézier timing curve for the callbacks.
If you really must, you should be able to do key-value observing on gearKnob.layer.presentationLayer.center. The layer returned by -[CALayer presentationLayer] is “a close approximation of the layer that is currently being displayed onscreen. While an animation is in progress, you can retrieve this object and use it to get the current values for those animations.”
you cannot use direct animation in that case
You have to use your own timer and move the knob
use : NSTimer timerWithTimeInterval:
and in the selector move the knob and get the position
Related
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 have the following routine to simply animate an image across the screen.
-(void)animateGrasland {
[UIView animateWithDuration:5 delay:0.0 options:UIViewAnimationOptionCurveLinear
animations:^{
CGRect newFrame = CGRectMake(100,100,1024,768);
self.grasland.frame = newFrame;
}
completion:^(BOOL finished) {
}
];
}
I want to start the animation immediately after I load the View. And to last for 5 seconds. So I have been experimenting with several options to calls of the function.
Option I - This option immediately shows the image at the new position, no animation!
- (void)viewDidLoad {
[super viewDidLoad];
[self animateGrasland];
}
Option II - This option animates the image to the new position. Bingo.
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:#selector(animateGrasland) userInfo:nil repeats:NO];
}
I have bene playing with the delay in the Time a little and for 0.001 sec the animation will start. If the duration is 0.0001 sec the image is displayed immediately at the new position, so without animation. I expect that the duration of the parsing of the viewDidLoad (and other background routines causes this).
So because the initial position (0,0), which is set in the storyboard, is updated during the viewDidLoad, the Animation is not triggered. And the animation moves immediately to its finished state.
Try calling animateGrasland in viewDidAppear: instead of viewDidLoad, like this:
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated]
[self animateGrasland];
}
// Remove your implementation of the viewDidLoad method or at least the call to animateGrasland
viewDidLoad is called after the view is loaded into the memory, while viewDidAppear: is called after the view is added to the view hierarchy. In your case, this means right after the view is visible. Since you want your animation to start as soon as the view is visible, you should start it in viewDidAppear:
I have a method called by performSelector:withObject:afterDelay:, which performs animation with duration. So after the delay, animation runs but without animation duration:
[self performSelector:#selector(animate:) withObject:[NSNumber numberWithInt:-1] afterDelay:4.0];
-(void) animate:(int) mode
{
[UIView animateWithDuration:0.2
delay:0.0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
self.center = CGPointMake(160.0, 568.0 + mode*height/2);
}
completion:nil];
}
Also, I cannot nest animations in completion: because it blocks UI interaction.
I've tried your code and animation duration works great but I think you should fix the (int) mode part. You can't cast NSNumber to int that way. Your method must be - (void)animate:(NSNumber*)mode and after you can cast to int with [mode intValue].
You cannot use performSelector:withObject:afterDelay: with animate: because it has a parameter that is not object-pointer type.
Hi everyone,
small question.
I have an UIView class that I created, AnimatedUIView.
In the init, I create a subview that I animate like this:
- (void)animateView{
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
[self animateView];
}
];
}
];
}
I have to call [self animateView] myself in the second completion block rather than using option repeat because I want to delay 1 second only the first animation.
Everything works fine, I alloc and init my view and add it as a subview, and it is animated as should.
But, when I close my superview, I guess ARC does its work and deallocs the animatedView, but my CPU goes 100 % !
I have investigated and if I comment the call to [self animateView], my CPU doesn't skyrocket when I close the superview.
So I've managed to solve the problem (by putting a condition before the call and changing a boolean value before closing the superview, keepAnimating = NO) , I just wanted to understand WHY it does this?
Why does it try to keep animating, and why does it use so much CPU?
If I put an NSLog in the completion block, I first see it every 8 seconds, but when I close the superview, the NSLog just keeps appearing every ms...
BTW: it relates to this question : UIView animation using 90%CPU , which was not really answered. Thanks a lot!
CPU going to 100% almost always means infinite recursion.
We can only guess because only Apple knows what's inside the animation source code.
In this case I guess this could be caused by triggering the animation when the animation cannot run anymore (e.g. there is no layer since we are in dealloc state). That would mean the completion handler is called immediately and that leads to infinite recursion.
There are safer ways to create infinite animations (e.g. using CAAnimation on the layer).
You should not use self in side the block. It will create a retain cycle. If it is necessary to call self create a weak self pointer. Code like this
__weak typeof(self) weakself = self;
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
[weakself animateView];
}
];
}
];
But main issue with your code is you calling animateView recursively without any base case. So i consuming CPU cycles... do not use animateView without base case.
Try using timer
Write following line in your viewDidLoad or any where else from where you want animation to start.
[NSTimer scheduledTimerWithTimeInterval:9 target:self selector:#selector(animateView) userInfo:nil repeats:YES];
// 9 is total animation duration.....
update your animateView method as follows:
- (void)animateView{
[UIView animateWithDuration:4.0
delay:1.0
options: nil
animations:^{
// Animation PART 1
}
completion:^(BOOL completed){
[UIView animateWithDuration:4.0
animations:^{
// Animation PART 2
}
completion:^(BOOL completed){
}
];
}
];
}
I'm currently taking my first shaky steps in ios development. I'm trying to animate a free falling ball. I have a button connected to an IBAction, and and UIImageView containing an image of my ball.
Inside my action, I have a while loop that's timed using NSTimeInterval, and based on the time it takes it calculates a new position until the ball reaches 'the ground'(Yes, I realize this is probably the least optimal way to do it. But I'm having a hard time grasping the syntax (and therein my problem probably lays), the optimisation will have to come later). From what I can understand, NSTimeInterval returns the elapsed time in seconds, so even though it will increment incredibly small steps, it should work. But I may have a serious case of brain fart.
So far so good. But when I tap the button, the ball moves straight from it's starting point to it's finishing point without an animation.
-(IBAction)doshit:(id)sender{
int G = 10;
CGPoint center = [myImage center];
NSDate *startTime = [NSDate date];
NSTimeInterval T;
T = fabs([startTime timeIntervalSinceNow]);
while (center.y<480)
{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:T];
center.y=center.y+((G*T*T)/2);
[myImage setCenter:center];
T = fabs([startTime timeIntervalSinceNow]);
[UIView commitAnimations];
}
}
I welcome all suggestions! =)
One way to do it is to use a CADisplayLink to provide the timing -- it is tied to the display refresh rate of 1/60 of a second. If you 're using auto layout (which is on by default now), it is better to animate a constraint, rather then set a frame. So, this example uses a button at the top of the screen, whose constraint to the top of the view is connected to the IBOutlet topCon.
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#interface ViewController ()
#property (nonatomic, strong) CADisplayLink *displayLink;
#property (weak,nonatomic) IBOutlet NSLayoutConstraint *topCon;
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
[self performSelector:#selector(startDisplayLink) withObject:nil afterDelay:2];
}
- (void)startDisplayLink {
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(handleDisplayLink:)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}
- (void)stopDisplayLink {
[self.displayLink invalidate];
self.displayLink = nil;
}
- (void)handleDisplayLink:(CADisplayLink *)displayLink {
static BOOL first = YES;
static double startTime = 0;
if (first) {
startTime = displayLink.timestamp;
}
first = NO;
double T = (double)displayLink.timestamp - startTime;
self.topCon.constant += ((10 * T * T)/2);
if (self.topCon.constant > 420) {
[self stopDisplayLink];
}
}
As Carl notes, you cannot perform animations by repeatedly changing things in the middle of a method call. See What is the most robust way to force a UIView to redraw? for more discussion on that.
As you may suspect, not only is this non-optimal, it actively fights iOS. iOS has many easy-to-use techniques for performing smooth animations without resorting to timers of any kind. Here is one simple approach:
- (IBAction)dropAnimate:(id)sender {
[UIView animateWithDuration:3 animations:^{
self.circleView.center = CGPointMake(100, 300);
}];
}
In the animations block, you change the thing you want to change to the final value (myImage.center in your case). UIKit will sample the current value and the final value, and figure out a path to get you there in the time you requested. For a full, runnable example with a few other features (like chained animations), see the example code from iOS:PTL Chapter 9.
The above code will use the default timing function. Once you understand that, you can move on to customizing the timing function. See Animation Pacing in the Animation Types and Timing Programming Guide. Also How to create custom easing function with Core Animation? and Parametric acceleration curves in Core Animation. But I would get your head around simple, default animations first.