How to prevent UIAnimation from interfering with CADisplayLink - ios

In my current project there are two main mechanics. A series of objects that continually move up the screen, and a mechanic where you press multiple buttons to move 2 objects. For the series of objects that move up the screen, i have utilized a CADisplayLink, and for the other mechanic is use a UIAnimation, however when i run my project, i notice that the UIAnimation, interferes with the movement of the Objects linked with the CADisplayLink for a split second whenever a button is pressed. How do I correct this issue??
Below is my code for these two mechanics
-(IBAction)tap{
CGRect frame = Person.frame;
frame.origin.x = 16;
frame.origin.y = 37;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
Person.frame = frame;
[UIView commitAnimations];
}
-(IBAction)tap1{
CGRect frame = Person.frame;
frame.origin.x = 241;
frame.origin.y = 37;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
Person.frame = frame;
[UIView commitAnimations];
}
-(IBAction)tapR1{
CGRect frame = Person1.frame;
frame.origin.x = 302;
frame.origin.y = 37;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
Person1.frame = frame;
[UIView commitAnimations];
}
-(IBAction)tapR2{
CGRect frame = Person1.frame;
frame.origin.x = 526;
frame.origin.y = 37;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.2];
Person1.frame = frame;
[UIView commitAnimations];
}
-(void)GameOver{
}
-(void)Score{
ScoreNumber = ScoreNumber + 1;
ScoreLabel.text = [NSString stringWithFormat:#"%i", ScoreNumber];
}
-(IBAction)StartGame:(id)sender{
StartGame.hidden = YES;
Spike.hidden = NO;
Spike1.hidden = NO;
Spike2.hidden = YES;
SpikeR.hidden = NO;
SpikeR1.hidden = NO;
SpikeR2.hidden = NO;
circle.hidden = NO;
ScoreLabel.hidden = NO;
[self PlaceBars];
[self PlaceBars1];
Movement = [CADisplayLink displayLinkWithTarget:self selector:#selector(BarMoving)];
[Movement addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
-(void)BarMoving{
Spike.center = CGPointMake(Spike.center.x, Spike.center.y - 5);
Spike1.center = CGPointMake(Spike1.center.x, Spike1.center.y - 5);
Spike2.center = CGPointMake(Spike1.center.x, Spike1.center.y - 5);
if (Spike2.center.y == -115) {
[self PlaceBars];
}
SpikeR.center = CGPointMake(SpikeR.center.x, SpikeR.center.y - 5);
SpikeR1.center = CGPointMake(SpikeR1.center.x, SpikeR1.center.y - 5);
SpikeR2.center = CGPointMake(SpikeR2.center.x, SpikeR2.center.y - 5);
if (SpikeR2.center.y == -115) {
[self PlaceBars1];
}
}

I rekon you use CADisplayLink for all the movement mechanisms.
However, delegate the logic of movement to your objects, instead of having a parent object that manages all movements. This is the pattern that you will see in Unity.
In your case, when CADisplayLink updates, loop through your objects and call update() method on each of them. This update method moves that object up 5 points, like what BarMoving() does, on normal state. On tapped state, it moves to [241, 37] gradually.
When user taps the object, change it state from normal to tapped, setting initial velocity, position algorithms, target point, and update() method mentioned above will handle the tap movement.

Related

Animation of Views at ViewController Load

I am performing an animation when one of my ViewControllers loads. I am calling it in the viewDidAppear method. It looks like this.
[UIView animateKeyframesWithDuration:2.5f delay:0.0f options:0 animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:1.0f animations:^{
CGRect rocketFrame = _rocketImageView.frame;
rocketFrame.origin.y = rocketFinishPosition;
_rocketImageView.frame = rocketFrame;
CGRect flameFrame = _flameImageView.frame;
flameFrame.origin.y = rocketFinishPosition + rocketFrame.size.height - 3;
_flameImageView.frame = flameFrame;
_flameImageView.hidden = false;
CGRect earthFrame = _earthImageView.frame;
earthFrame.origin.y = earthFinishPosition;
_earthImageView.frame = earthFrame;
CGRect moonFrame = _moonImageView.frame;
moonFrame.origin.y = moonFinishPosition;
_moonImageView.frame = moonFrame;
}];
[UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.6f animations:^{
_flameImageView.transform = CGAffineTransformMakeScale(1.f, 1.f);
}];
} completion:nil];
The issue is when I first load the ViewController, the animation plays but then when finished it snaps back to the starting position. However if I navigate away from the ViewController and back again, the animation plays and the changes persist.
My Question
Are my subviews being played out twice?
When can I be certain that my ViewController has finished loading?
Assuming not-autolayout, in the completion block, set the view properties to the final, desired states...
// taking the rocket frame as an example:
CGRect rocketFrame = _rocketImageView.frame; // do this with the other frames, too
[UIView animateKeyframesWithDuration:2.5f delay:0.0f options:0 animations:^{
[UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:1.0f animations:^{
rocketFrame.origin.y = rocketFinishPosition;
_rocketImageView.frame = rocketFrame;
CGRect flameFrame = _flameImageView.frame;
flameFrame.origin.y = rocketFinishPosition + rocketFrame.size.height - 3;
_flameImageView.frame = flameFrame;
_flameImageView.hidden = false;
CGRect earthFrame = _earthImageView.frame;
earthFrame.origin.y = earthFinishPosition;
_earthImageView.frame = earthFrame;
CGRect moonFrame = _moonImageView.frame;
moonFrame.origin.y = moonFinishPosition;
_moonImageView.frame = moonFrame;
}];
[UIView addKeyframeWithRelativeStartTime:0.0f relativeDuration:0.6f animations:^{
_flameImageView.transform = CGAffineTransformMakeScale(1.f, 1.f);
}];
} completion:^(BOOL finished) {
// do this with the other frames, too
_rocketImageView.frame = rocketFrame;
}];

UIView beginAnimations in one instance of a class fires beginAnimations in another instance of same class

Hope you can help or explain what's going on here, as I'm not great with UIGraphics contexts.
I have defined a class that automatically scrolls a label back and forth in it's frame when the label text is to long for the given space on the view controllers primary view.
It consists of a scrollview that fits the allocated space in the parent view, and contains a UILabel (subclassed) that is sized to fit it's text.
(The UILabel is a subview in the parent scrollview).
The scrollview commits an animation that scrolls the UILabel left -> right, then when the animation is finished, I fire the delegate '
(void)animationDidStop:animationID(NSString*)finished:(NSNumber *)finished context:(void *)context'
which resets some numbers and restarted the animation scrolling it in the reverse direction right -> left
This works really nicely.
However, if i add another instance of this 'scrolling label' somewhere else in the view controllers master view, as one of the animations stop and the AnimationDidStop delegate gets fired, it relaunches the animations from scratch for the other 'scrolling' label object.
I have tried the following to isolate the firing of the animation with no positive results.
Passing the views context and an identifier to beginAnimations and then trapping them in the (void)animationDidStop method... Didn't make any difference.
This makes me think the method doesn't seem to take
Below is the code that does the animation, and the code that relaunches it in the other direction.
As I say, it works really well on it's own, but when there is more than 1 instance of this class resident on the screen at the same time, the beginAnimations seems to fire for both instances.
Hope you can explain why. Thanks.
- (void)beginAnimationWithOrgigin:(CGPoint)origin Terminus:(CGPoint)terminus {
NSLog (#"Message %# in context %#",((UILabel*)_textLabel).text,_ctx);
CGFloat text_width = ((UILabel*)_textLabel).frame.size.width;
CGFloat display_width = self.frame.size.width;
if ( text_width > display_width ) {
float duration = (text_width - display_width)/40;
[self setContentOffset:origin];
[UIView beginAnimations:((UILabel*)_textLabel).text context:_ctx];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelay:1.0];
[UIView setAnimationDuration:duration];
[UIView setAnimationRepeatCount:1];
[self setContentOffset:terminus];
[UIView commitAnimations];
}
}
- (void)animationDidStop:(NSString *)animationID
finished:(NSNumber *)finished
context:(void *)context
{
NSLog(#"Animation Did stop context = %#",context);
if ([animationID isEqualToString:((UILabel*)_textLabel).text]){
static BOOL forward = NO;
if ([finished boolValue]) {
CGPoint origin;
CGPoint terminal_origin;
if (forward){
origin = CGPointMake(0, 0);
terminal_origin = CGPointMake(((UILabel*)_textLabel).frame.size.width - self.frame.size.width, ((UILabel*)_textLabel).frame.origin.y);
}else{
terminal_origin = CGPointMake(0, 0);
origin = CGPointMake(((UILabel*)_textLabel).frame.size.width - self.frame.size.width, ((UILabel*)_textLabel).frame.origin.y);
}
forward = !forward;
[self beginAnimationWithOrgigin:origin Terminus:terminal_origin];
}
}
}
EDIT: Added 28/5/14 # 13:14
After A-Lives advise regarding the use of statics... have modified the code to use it's label as a flag to prompt the change of direction in scrolling.
THIS HAS FIXED THE PROBLEM
- (void)beginAnimationWithOrgigin:(CGPoint)origin Terminus:(CGPoint)terminus Direction:(NSString*)direction{
if (!direction)direction = #"FORWARD";
NSLog (#"Message %# in context %#",((UILabel*)_textLabel).text,_ctx);
CGFloat text_width = ((UILabel*)_textLabel).frame.size.width;
CGFloat display_width = self.frame.size.width;
if ( text_width > display_width ) {
float duration = (text_width - display_width)/40;
[self setContentOffset:origin];
[UIView beginAnimations:direction context:_ctx];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelay:1.0];
[UIView setAnimationDuration:duration];
[UIView setAnimationRepeatCount:1];
[self setContentOffset:terminus];
[UIView commitAnimations];
}
}
- (void)animationDidStop:(NSString *)animationID
finished:(NSNumber *)finished
context:(void *)context
{
NSLog(#"Animation Did stop context = %#",context);
NSString *direction;
CGPoint origin;
CGPoint terminal_origin;
if (![animationID isEqualToString:#"FORWARD"]){
origin = CGPointMake(0, 0);
terminal_origin = CGPointMake(((UILabel*)_textLabel).frame.size.width - self.frame.size.width, ((UILabel*)_textLabel).frame.origin.y);
direction = #"FORWARD";
}else{
terminal_origin = CGPointMake(0, 0);
origin = CGPointMake(((UILabel*)_textLabel).frame.size.width - self.frame.size.width, ((UILabel*)_textLabel).frame.origin.y);
direction = #"BACKWARD";
}
[self beginAnimationWithOrgigin:origin Terminus:terminal_origin Direction:direction];
}
The problem was in forgetting that statics really are just ordinary C static variables which are globally accessible across the app.
(i.e, not encapsulated in the scope that defined them.)
Multiple instances of the same class are all reading and writing values to a the same variable causing unpredictable behaviour.
Solution: Pass instance level messages to control toggling activities where you can't guarantee your class is a singleton.
Modified Working Code:
- (void)beginAnimationWithOrgigin:(CGPoint)origin Terminus:(CGPoint)terminus Direction:(NSString*)direction{
if (!direction)direction = #"FORWARD";
NSLog (#"Message %# in context %#",((UILabel*)_textLabel).text,_ctx);
CGFloat text_width = ((UILabel*)_textLabel).frame.size.width;
CGFloat display_width = self.frame.size.width;
if ( text_width > display_width ) {
float duration = (text_width - display_width)/40;
[self setContentOffset:origin];
[UIView beginAnimations:direction context:_ctx];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:#selector(animationDidStop:finished:context:)];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDelay:1.0];
[UIView setAnimationDuration:duration];
[UIView setAnimationRepeatCount:1];
[self setContentOffset:terminus];
[UIView commitAnimations];
}
}
- (void)animationDidStop:(NSString *)animationID
finished:(NSNumber *)finished
context:(void *)context
{
NSLog(#"Animation Did stop context = %#",context);
NSString *direction;
CGPoint origin;
CGPoint terminal_origin;
if (![animationID isEqualToString:#"FORWARD"]){
origin = CGPointMake(0, 0);
terminal_origin = CGPointMake(((UILabel*)_textLabel).frame.size.width - self.frame.size.width, ((UILabel*)_textLabel).frame.origin.y);
direction = #"FORWARD";
}else{
terminal_origin = CGPointMake(0, 0);
origin = CGPointMake(((UILabel*)_textLabel).frame.size.width - self.frame.size.width, ((UILabel*)_textLabel).frame.origin.y);
direction = #"BACKWARD";
}
[self beginAnimationWithOrgigin:origin Terminus:terminal_origin Direction:direction];
}

Smoothest Way To Implement Back to Back UIAnimations

I need to have my character jump (increase the origin.y by 50 and reverse) in a game I am creating.
So far I have come across two ways of doing this:
Method 1:
[UIView animateWithDuration:1.0
delay:0.0
options:UIViewAnimationOptionAutoreverse
animations:^{
CGRect frame = self.kevinImageView.frame;
frame.origin.y -= 50;
self.kevinImageView.frame = frame;
} completion:^(BOOL finished){
CGRect frame = self.kevinImageView.frame;
frame.origin.y = 135;
self.kevinImageView.frame = frame;
}];
Issues: At the end of every complete jump animation (up and down) the ImageView jumps up and then back down (due probably to the tiny amount it takes for the completion block to be called). This is noticeable and I would much rather do without it.
Method 2:
[UIView animateWithDuration:1.0
animations:^{
CGRect frame = self.kevinImageView.frame;
frame.origin.y -= 50;
self.kevinImageView.frame = frame;
} completion:^(BOOL finished){
[UIView animateWithDuration:1.0
animations:^{
CGRect frame = self.kevinImageView.frame;
frame.origin.y += 50;
self.kevinImageView.frame = frame;
}];
}];
Issues: If I tap the view (I use a UITapGuestureRecognizer) and the ImageView is in the completion animation (going back down), the Image View will snap back to the bottom instead of finishing its animation. Again not a major issue, but something which I should be able to avoid.
Is there any method I have not come across which will resolve both of these issues, or a way to fix one of the methods?
Can you disable your UITapGuestureRecognizer when the animation is still running? And then enable it when the animation finishes.
if (!self.isAnimationRunning) {
self.isAnimationRunning = YES;
[UIView animateWithDuration:1.0
animations:^{
CGRect frame = self.kevinImageView.frame;
frame.origin.y -= 50;
self.kevinImageView.frame = frame;
} completion:^(BOOL finished){
[UIView animateWithDuration:1.0
animations:^{
CGRect frame = self.kevinImageView.frame;
frame.origin.y += 50;
self.kevinImageView.frame = frame;
} completion:^(BOOL finished){
self.isAnimationRunning = NO;
}];
}];
}
I'm very interested in this question so I do a simple demo to do a back to back animation.
I think maybe you can just use UIViewAnimationOptionAutoreverse and UIViewAnimationRepeat option to achieve a autoreverse animation.
[UIView animateWithDuration:1.0
delay:0
options:UIViewAnimationOptionAutoreverse|UIViewAnimationRepeat
animations:^{
[UIView setAnimationRepeatCount:1];
self.kevinImageView.center = CGPointMake(self.kevinImageView.center.x ,self.kevinImageView.center.y - 25);
} completion:^(BOOL finished){
self.kevinImageView.center = CGPointMake(self.kevinImageView.center.x ,self.kevinImageView.center.y + 25);
}];
and you can also set UIViewAnimationOptionAllowUserInteraction if you want to enable use interaction during the animation.

UIView animateWithDuration doesn't work with Rect width/height animation

The following code works. It animates all those components nicely
CGRect logoRect = self.logoImageView.frame;
CGRect loginBackgroundRect = self.loginControlsBkImageView.frame;
CGRect loginButtonRect = self.loginButton.frame;
CGRect tableViewRect = self.tableView.frame;
CGRect forgotPasswordRect = self.forgotButton.frame;
CGRect signupButtonRect = self.signUpButton.frame;
if (!iPhone) {
// ipad keyboard-on-screen re-layout
logoRect.origin.y-= 60;
loginBackgroundRect.origin.y-= 110;
loginButtonRect.origin.y-=110;
tableViewRect.origin.y-=110;
forgotPasswordRect.origin.y-=110;
signupButtonRect.origin.y-=200;
}
else {
// iphone keyboard-on-screen re-layout
if (portrait) {
logoRect.origin.y-=17;
logoRect.origin.x-=50;
loginBackgroundRect.origin.y-= 70;
loginButtonRect.origin.y-=70;
tableViewRect.origin.y-=70;
forgotPasswordRect.origin.y-=70;
//signupButtonRect.origin.y+=200; // get off screen!
} else {
logoRect.origin.y-= 30;
loginBackgroundRect.origin.y-= 25;
loginButtonRect.origin.y-=25;
tableViewRect.origin.y-=25;
}
}
[UIView animateWithDuration:0.2f
delay:0.0f
options:UIViewAnimationOptionCurveEaseIn
animations:^(void) {
self.logoImageView.frame = logoRect;
self.loginControlsBkImageView.frame = loginBackgroundRect;
self.loginButton.frame = loginButtonRect;
self.tableView.frame = tableViewRect;
self.forgotButton.frame = forgotPasswordRect;
self.signUpButton.frame = signupButtonRect;
}
completion:NULL];
Take the following code and add one line (see below) to animate the WIDTH of the logoImageView... and puff... only the logoImageView animation works - the rest simply doesn't move. as if the frame size animation causes everything else not to animate if in the same animation block.
if (portrait) {
logoRect.origin.y-=17;
logoRect.origin.x-=50;
loginBackgroundRect.origin.y-= 70;
loginButtonRect.origin.y-=70;
tableViewRect.origin.y-=70;
forgotPasswordRect.origin.y-=70;
//signupButtonRect.origin.y+=200; // get off screen!
} else {
logoRect.origin.y-= 30;
logoRect.size.width-= 30; // <------- LINE BEING ADDED HERE
loginBackgroundRect.origin.y-= 25;
loginButtonRect.origin.y-=25;
tableViewRect.origin.y-=25;
}
I'm at a loss here. Does anyone know what's going on?
Try to change whole frame, not just width. It should work.
Put these lines before commiting animation (not in block):
self.logoImageView.frame = logoRect;
etc.
And instead of using animate method try to use commit animations this way:
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.75];
// Here some more animation settings
// Here your animations
[UIView commitAnimations];

My animatewithduration, completion block is performed only once

I use the following block of code to slide a UIView down and when finished rotate another UIView.
The second part of the animation, the completion block is only performed once which means the 1st animation is not completed else it would reach the completion block.
On the iphone simulator it looks as if the 1st animation did finish...
can anyone help me figure this out?
my NSLog says:
finished 1st
started 2nd
finished 1st
finished 1st
finished 1st
.
.
.
- (IBAction) move
{
[UIView animateWithDuration:0.7 animations:^{
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.7];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:NO];
CGPoint pos = movingtTable.center;
float moveDistance = 220.0;
if(!isViewVisible){
//expose the view
pos.y = pos.y+moveDistance;
//disable selection for xy table
xTable.userInteractionEnabled = NO;
yTable.userInteractionEnabled = NO;
//angle = M_PI;
}
else
{
pos.y = pos.y-moveDistance;
xTable.userInteractionEnabled = YES;
yTable.userInteractionEnabled = YES;
//angle = -M_PI;
}
isViewVisible = !isViewVisible;
movingtTable.center = pos;
NSLog(#"finished 1st");
}completion:^(BOOL finished){
NSLog(#"started 2nd");
[UIView animateWithDuration:0.4 animations:^{
//[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.4];
//[UIView setAnimationRepeatCount:1];
//[UIView setAnimationRepeatAutoreverses:NO];
arrows.transform = CGAffineTransformMakeRotation(angle);
}completion:^(BOOL finished){
angle = -angle;
}];
}];
Why are you trying to initialize another UIView animation inside the animateWithDuration block code? Update your code to the following and make sure you're not performing multiple animations of a single view at a time.
- (IBAction) move
{
[UIView animateWithDuration:0.7 animations:^{
CGPoint pos = movingtTable.center;
float moveDistance = 220.0;
if(!isViewVisible){
//expose the view
pos.y = pos.y+moveDistance;
//disable selection for xy table
xTable.userInteractionEnabled = NO;
yTable.userInteractionEnabled = NO;
//angle = M_PI;
}
else
{
pos.y = pos.y-moveDistance;
xTable.userInteractionEnabled = YES;
yTable.userInteractionEnabled = YES;
//angle = -M_PI;
}
isViewVisible = !isViewVisible;
movingtTable.center = pos;
NSLog(#"finished 1st");
}
completion:^(BOOL finished){
NSLog(#"started 2nd");
[UIView animateWithDuration:0.4 animations:^{
arrows.transform = CGAffineTransformMakeRotation(angle);
}completion:^(BOOL finished){
angle = -angle;
}];
}];
BTW: The block code requires some serious refactoring, if you ask me :)
You are mixing and matching paradigms and I believe that is causing the issue you are seeing. You are creating an animation block, but inside of that block you are creating a new animation routine with the 'old' paradigm for running UIView animations. Apple is leading people away from the old paradigm and I would encourage you to ONLY use blocks as well.
This is why the completion block only runs once, the UIView animateWith block code only runs once. However, your internal animation code runs multiple times.
Take out:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:0.7];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:NO];
If you want your animation block to run several times, then use the full method:
animateWithDuration:delay:options:animations:completion:
Make the delay = 0, and set your options to UIViewAnimationOptionRepeat, or whatever you need to accomplish the number of cycles you want the block to complete.
Here is my suggestion assuming you want it to repeat:
- (IBAction) move
{
[UIView animateWithDuration:0.7
delay:0
options:UIViewAnimationOptionRepeat
animations:^{
CGPoint pos = movingtTable.center;
float moveDistance = 220.0;
if(!isViewVisible) {
//expose the view
pos.y = pos.y+moveDistance;
//disable selection for xy table
xTable.userInteractionEnabled = NO;
yTable.userInteractionEnabled = NO;
//angle = M_PI;
}
else {
pos.y = pos.y-moveDistance;
xTable.userInteractionEnabled = YES;
yTable.userInteractionEnabled = YES;
//angle = -M_PI;
}
isViewVisible = !isViewVisible;
movingtTable.center = pos;
NSLog(#"finished 1st");
}
completion:^(BOOL finished){
NSLog(#"started 2nd");
[UIView animateWithDuration:0.4
animations:^{
arrows.transform = CGAffineTransformMakeRotation(angle);
}
completion:^(BOOL finished){
angle = -angle;
}];
}];
}

Resources