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.
Related
I am using Google Maps iOS sdk for my app .In my app user can draw a fence(a circle) and later can edit to change and resize the radius of circle .
Its resizing properly but when radius value changes its instant,not a smooth animation like map zoom in/out.Is it achievable with latest GMaps sdk for ios?
Apparently, its not possible becuase what i see is GMSCircle is inherited from GMSOverlay which is child of NSObject,so its defineltly not a view,rather that overlay is drawn with some layer or something like that .
Any help is appreciated..!!
Thanks..!!
I found that you could just change the radius, and the circle changes.
So I wrote a helper class to do this:
#interface TAMapCircle : GMSCircle
{
CLLocationDistance _from;
CLLocationDistance _to;
NSTimeInterval _duration;
}
#property (nonatomic, copy) void(^handler)();
#property (nonatomic, strong) NSDate * begin;
#end
#implementation TAMapCircle
// just call this
-(void)beginRadiusAnimationFrom:(CLLocationDistance)from
to:(CLLocationDistance)to
duration:(NSTimeInterval)duration
completeHandler:(void(^)())completeHandler {
self.handler = completeHandler;
self.begin = [NSDate date];
_from = from;
_to = to;
_duration = duration;
[self performSelectorOnMainThread:#selector(updateSelf) withObject:nil waitUntilDone:NO];
}
// internal update
-(void)updateSelf {
NSTimeInterval i = [[NSDate date] timeIntervalSinceDate:_begin];
if (i >= _duration) {
self.radius = _to;
self.handler();
return;
} else {
CLLocationDistance d = (_to - _from) * i / _duration + _from;
self.radius = d;
// do it again at next run loop
[self performSelectorOnMainThread:#selector(updateSelf) withObject:nil waitUntilDone:NO];
}
}
#end
Hopefully my answer can help.
I have done something similar using CADisplayLink. It makes it very easy to do simple animations in a GMSMapView.
https://developer.apple.com/library/prerelease/ios/documentation/QuartzCore/Reference/CADisplayLink_ClassRef/index.html
You need to wrap the animation changes that you are attempting to make in UIView animation block. This has changed in the latest version of the iOS SDK such that the method signature is [UIView beginAnimation:]. Wrap any changes you make in this block and they will be animated by UIKit. If that doesn't work you can go to a lower level with CoreAnimation transactions.
Short description: When the keyboard is shown I translate a button up, but upon clicking the button it pops back down to its original position.
Long description: I have two buttons, one on top of the other. One is hidden until certain criteria are met, then it becomes visible and the other is hidden.
#property (weak, nonatomic) IBOutlet UIButton *skipButton;
#property (weak, nonatomic) IBOutlet UIButton *saveButton;
#property (assign, nonatomic) BOOL saveButtonEnabled;
#property (assign, readonly, nonatomic) CGRect fieldContainerDefaultFrame;
#property (assign, readonly, nonatomic) CGRect skipButtonDefaultFrame;
When view loads I cache the default positions
- (void)viewDidLoad
{
[super viewDidLoad];
...
_fieldContainerDefaultFrame = self.fieldContainerView.frame;
_skipButtonDefaultFrame = self.skipButton.frame;
[self.saveButton setHidden:YES];
self.saveButtonEnabled = NO;
}
The buttons are wired to outlets correctly and these are their "touch up inside" actions
#pragma mark - Actions
- (IBAction)didPressSaveButton:(id)sender
{
// Code here to persist data
// Code here to push segue to next scene
}
- (IBAction)didPressSkipButton:(id)sender
{
// Code here to push segue to next scene
}
Here are the methods that respond to the keyboard notifications. I do register for the notifications I'm just not including that code. (There is no issue there, all this stuff gets run correctly.)
NOTE: Both buttons are contained in the same "container" frame (along with other fields), which translates up. The buttons translate up an additional amount. The translation works in general, the only issue is the reseting behavior of the button upon click.
#pragma mark - Notification handlers
- (void)handleKeyboardWillShowNotification:(NSNotification *)note
{
if([self.bioTextView isFirstResponder])
{
CGRect newFieldContainerFrame = self.fieldContainerDefaultFrame;
CGRect newSkipButtonFrame = self.skipButtonDefaultFrame;
newFieldContainerFrame.origin.y += AGSTSignupDetailsFieldContainerEditingOffset;
newSkipButtonFrame.origin.y += AGSTSignupDetailsSkipButtonEditingOffset;
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve animationCurve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
[UIView animateWithDuration:duration animations:^{
[UIView setAnimationCurve:animationCurve];
self.fieldContainerView.frame = newFieldContainerFrame;
self.skipButton.frame = newSkipButtonFrame;
self.saveButton.frame = newSkipButtonFrame;
}];
}
}
- (void)handleKeyboardWillHideNotification:(NSNotification *)note
{
NSTimeInterval duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
UIViewAnimationCurve animationCurve = [note.userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue];
[UIView animateWithDuration:duration animations:^{
[UIView setAnimationCurve:animationCurve];
self.fieldContainerView.frame = self.fieldContainerDefaultFrame;
self.skipButton.frame = self.skipButtonDefaultFrame;
self.saveButton.frame = self.skipButtonDefaultFrame;
}];
}
I'd appreciate any insights or suggestions as to how to fix the bug or how to craft a better solution. Thanks.
Okay, I fixed the problem. While I appreciate rob mayoff's response as an indication of better practices (i.e., don't set frames when using autolayout), that was not the actual cause of the problem in my case, thus I don't consider my Q&A to be a duplicate of anything I could find. I'm going to post this answer in case someone else runs across a similar problem.
It turns out, as I suspected earlier this morning, that the button's push segue did two mutually incompatible things: (1) it caused the keyboard to disappear, which caused the fields to be translated downward; and (2) it caused the whole scene to animate away as the new VC comes into place, which apparently cancelled all pending animations (i.e., the translation) and caused those translations to abruptly jump to their destination states.
The quickest solution was to set a BOOL didPressSaveButton to NO on viewDidLoad and only set it to YES once the button is pushed, and then check that BOOL before every relevant translation animation: if the BOOL is NO, go ahead and do the animation, otherwise do nothing. This will prevent the abrupt final animation before the push segue.
The better solution, and one I will implement soon, will be to replace my use of frames with autolayout constraints.
I'm working on code for an expandable tray view that uses UIDynamicAnimator to achieve a nice expand/contract animation.
To achieve a realistic acceleration I use UIGravityBehavior to make my tray fall, until the "tab" of the tray hits the bottom of the screen.
This works well, but even though all items in the scene have stopped moving, UIDynamicAnimatorDelegate dynamicAnimatorDidPause: is never called. This means that the animator continues using CPU cycles to animate the scene ( the delegate is set, and fires for UIDynamicAnimatorDelegate dynamicAnimatorDidPause: ).
I tried removing the UIGravityBehavior from the scene, which did indeed cause the animator to stop in the end. I can't time the removal of the gravity behavior right though, since I need to remove it from the scene once everything has stopped moving.
I understand that gravity is a constant force, but I still assumed it would stop the animator once everything has 0 velocity and 0 acceleration.
Is this last assumption false?
Anyone having similar problems?
You are correct that the animator should pause once everything comes to rest.
Check what items are attached to your gravity behavior, and make sure that there aren't other items still falling. For example, it is easy to accidentally create the following bug:
Add a view to gravity and collision
Remove view from superview and from collision
Fail to remove view from gravity
In this situation, the "ghost item" will fall forever.
Another possible problem (though less likely given your description) is if your items are attached to other behaviors that are causing infinite but small "bounce." I would check the full list of behaviors on your animator (remember to check child behaviors, too). In particular I'd be interested in any UIDynamicItemBehavior that adds elasticity.
EDIT:
You may also want to go the other way. Start with a very basic dynamics system and add components from yours until you can reproduce the problem. For instance, the following does converge quite quickly (logging "pause"):
#interface PTLViewController () <UIDynamicAnimatorDelegate>
#property (nonatomic, strong) UIDynamicAnimator *animator;
#end
#implementation PTLViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(100,100,100,100)];
view.backgroundColor = [UIColor lightGrayColor];
[self.view addSubview:view];
self.animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
self.animator.delegate = self;
UICollisionBehavior *collisionBehavior = [[UICollisionBehavior alloc] initWithItems:#[view]];
collisionBehavior.translatesReferenceBoundsIntoBoundary = YES;
[self.animator addBehavior:collisionBehavior];
UIGravityBehavior *gravityBehavior = [[UIGravityBehavior alloc] initWithItems:#[view]];
[self.animator addBehavior:gravityBehavior];
}
- (void)dynamicAnimatorDidPause:(UIDynamicAnimator *)animator {
NSLog(#"pause");
}
#end
To your question about getting all item velocities, I don't know of an easy way to do that. Unfortunately, UIDynamicAnimator doesn't directly know all of its items. This is indirectly because UIDyanamicBehavior doesn't include an items property. If this bothers you as much as it does me, consider duping radar://15054405.
But there is a solution if you just want to know the current linear velocity of specific items. Just add a UIDynamicItemBehavior with a custom action to log it:
UIDynamicItemBehavior *dynamicItemBehavior = [[UIDynamicItemBehavior alloc] initWithItems:#[view]];
__weak UIDynamicItemBehavior *weakBehavior = dynamicItemBehavior;
dynamicItemBehavior.action = ^{
NSLog(#"Velocity: %#", NSStringFromCGPoint([weakBehavior linearVelocityForItem:view]));
};
[self.animator addBehavior:dynamicItemBehavior];
I had a similar issue recently. In the end I used a UICollisionBehavior with boundaries instead of items (because otherwise the moving items were bumping the others...) and implement the delegate method collisionBehavior:beganContactForItem:withBoundaryIdentifier:atPoint: to know when I should remove the gravity
UICollisionBehavior *collide = [[[UICollisionBehavior alloc] initWithItems:borders] retain];
[collide addItem:movingItem];
[collide setCollisionMode:UICollisionBehaviorModeBoundaries];
[collide setTranslatesReferenceBoundsIntoBoundary:YES];
If you find a better solution, let me know :) !
My problem is the same. My animator never comes to rest so once started, my app consumes 3 to 4% CPU forever. My views all appear to stop moving within 1/2 second. So rather than figure out why I'm not reaching equilibrium, I just hit it with a hammer and kill the animator with a timer. I give it 2 seconds.
- (void)createAnimator {
if (_timer) {
[_timer invalidate];
}
if (_animator) {
[_animator removeAllBehaviors];
}
_animator = [[UIDynamicAnimator alloc] initWithReferenceView:self.view];
// create all behaviors
// kill the animator in 2 seconds
_timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:#selector(killAnimator:) userInfo:nil repeats:NO];
}
- (void)killAnimator:(NSTimer *)timer {
[_animator removeAllBehaviors];
_animator = nil;
_timer = nil;
}
[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
I am currently working on a project that I built using the alpha C4 framework.
I am trying to make start an animation as soon as the app launches without having to use an type of interaction to get it going (i.e. touchesBegan)...
But unfortunately I cant figure it out.
In C4, the way to do this is to take advantage of the following method:
-(void)performSelector:withObject:afterDelay:
And, for the current version of C4, the best way to use this is like:
#import "C4WorkSpace.h"
#interface C4WorkSpace ()
-(void)methodToRunImmediately;
#end
#implementation C4WorkSpace {
C4Shape *circle;
}
-(void)setup {
circle = [C4Shape ellipse:CGRectMake(100, 100, 100, 100)];
[self.canvas addShape:circle];
[self performSelector:#selector(methodToRunImmediately) withObject:nil afterDelay:0.1];
}
-(void)methodToRunImmediately {
circle.animationDuration = 1.0f;
circle.animationOptions = AUTOREVERSE | REPEAT;
circle.center = CGPointMake(384, 512);
}
#end
This code will start your animations after a 1/10th of a second delay... which will look immediate.
The answer above was posted a long time ago, and we were able to implement a more simple approach for this that doesn't require knowing what selectors are. The code above can now be run with the following:
-(void)runMethod:afterDelay:
Such that, in C4, the original line:
[self performSelector:#selector(methodToRunImmediately)
withObject:nil
afterDelay:0.1];
... can be rewritten as:
[self runMethod:#"methodToRunImmediately" afterDelay:0.1];