Performance issue with DrawRect and NSTimer - iphone

I'm trying to make a slot machine animation where the reels spin. to do this, I'm using drawRect to draw images in a custom class that inherits from UIView. I'm using an nstimer to update the position of the images and calling [self setNeedsDisplay] to update the drawing. In the simulator, it looks very good, however, on the device, it is very laggy. I was wondering if i'm doing something wrong with my method of drawing or is there any better solutions.
- (void)drawRect:(CGRect)rect
{
[image1 drawInRect:CGRectMake(0, image1Position, 98, 80)];
[image2 drawInRect:CGRectMake(0, image2Position, 98, 80)];
[image3 drawInRect:CGRectMake(0, image3Position, 98, 80)];
}
- (void)spin
{
// move each position down by 10 px
image1Position -= MOVEMENT;
image2Position -= MOVEMENT;
image3Position -= MOVEMENT;
// if any of the position <= -60 reset to 180
if(image1Position < -50)
{
image1Position = 180;
}
if(image2Position < -50)
{
image2Position = 180;
}
if(image3Position < -50)
{
image3Position = 180;
}
[self setNeedsDisplay];
}
-(void)beginSpinAnimation
{
timer = [NSTimer scheduledTimerWithTimeInterval:SCROLL_TIME target:self selector:#selector(spin) userInfo:self repeats:YES];
}
My CoreAnimation Attempt with UIScrollView:
- (void) spinToNextReel
{
int y = self.contentOffset.y + 80;
// if the current >= last element reset to first position (-30)
if(y >= (80 *(elementCount+1) - 30))
{
y = -30;
}
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDelegate:self];
[UIView setAnimationDuration:SCROLL_TIME];
[UIView setAnimationCurve:UIViewAnimationCurveLinear];
self.contentOffset = CGPointMake(0, y);
[UIView commitAnimations];
if (!isSpinning && targetY == y)
{
NSLog(#"target is %d, y is %d", targetY, y);
if(timer)
{
[timer invalidate];
timer = nil;
}
[self playSound];
}
}

I would say research into CoreAnimation. It is made to do what you want to do here. It'll be much faster than what you are doing here.
As for it being slow, calling drawInRect isn't the fastest thing in the world. What is SCROLL_TIME?

You want to use CoreAnimation, it will be a lot easier, and more efficient. Having said that, if you insist on trying to manually animate this way you are doing a couple of thing wrong:
Do not attempt move a constant amount on fixed intervals, timer events can be delayed, and if they are that will result in your animation being uneven, since you are moving a constant amount per event, not per time interval. You should record the actual timestamp every time you animate, compare it to the previous timestamp, and move an appropriate number of pixels. This will result in even amounts of movement even if the events are delayed (effectively you will being dropping frames).
Do not use an NSTimer, use a CADisplayLink. That will tie your drawing to the native refresh rate of the system, and synchronize it with any other drawing that is going on.
I know I said it already, but learn and use CoreAnimation, you will be much happier.

Related

"Push" animation in Objective-C

I have two views: A and B. A is positioned at the top of the screen, B is positioned at the bottom of the screen.
When the user presses a button, view B animates upwards with a EaseInEaseOut bezier curve until it reaches y = 0. While B is on its way to its destination, it should push A up when it hits A. In other words, when B has passed a certain y coordinate (A's y origin + height) during its transition from bottom to top, A should stick to B so it seems B pushes A upwards.
What I have tried so far:
Register a target + selector to a CADisplayLink immediately after the user pressed the button. Inside this selector, request view B's y coordinate by accessing its presentationLayer and adjust A's y coordinate accordingly. However, this method turns out to be not accurate enough: the presentationLayer's frame is behind on B's current position on the screen (this is probably because -presentationLayer recalculates the position of the animating view on the current time, which takes longer than 1 frame). When I increase B's animation duration, this method works fine.
Register a target + selector to a CADisplayLink immediately after the user pressed the button. Inside this selector, calculate B's current y coordinate by solving the bezier equation for x = elapsed time / animation duration (which should return the quotient distance traveled / total distance). I used Apple's open source UnitBezier.h for this (http://opensource.apple.com/source/WebCore/WebCore-955.66/platform/graphics/UnitBezier.h). However, the results are not correct.
Any suggestions on what I can try next?
Two simple solutions:
Use animationWithDuration only: You can break your animation into two nested animations, using "ease in" to animate the moving of "B" up to "A", and then using "ease out" to animate the moving of "B" and "A" the rest of the way. The only trick here is to make sure the two duration values make sense so that the speed of the animation doesn't appear to change.
CGFloat animationDuration = 0.5;
CGFloat firstPortionDistance = self.b.frame.origin.y - (self.a.frame.origin.y + self.a.frame.size.height);
CGFloat secondPortionDistance = self.a.frame.size.height;
CGFloat firstPortionDuration = animationDuration * firstPortionDistance / (firstPortionDistance + secondPortionDistance);
CGFloat secondPortionDuration = animationDuration * secondPortionDistance / (firstPortionDistance + secondPortionDistance);
[UIView animateWithDuration:firstPortionDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseIn
animations:^{
CGRect frame = self.b.frame;
frame.origin.y -= firstPortionDistance;
self.b.frame = frame;
}
completion:^(BOOL finished) {
[UIView animateWithDuration:secondPortionDuration
delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
CGRect frame = self.b.frame;
frame.origin.y -= secondPortionDistance;
self.b.frame = frame;
frame = self.a.frame;
frame.origin.y -= secondPortionDistance;
self.a.frame = frame;
}
completion:nil];
}];
You can let animateWithDuration handle the full animation of "B", but then use CADisplayLink and use presentationLayer to retrieve B's current frame and to adjust A's frame accordingly:
[self startDisplayLink];
[UIView animateWithDuration:0.5
delay:0.0
options:UIViewAnimationOptionCurveEaseInOut
animations:^{
CGRect frame = self.b.frame;
frame.origin.y = self.a.frame.origin.y;
self.b.frame = frame;
}
completion:^(BOOL finished) {
[self stopDisplayLink];
}];
where the methods to start, stop, and handle the display link are defined as follows:
- (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
{
CALayer *presentationLayer = self.b.layer.presentationLayer;
if (presentationLayer.frame.origin.y < (self.a.frame.origin.y + self.a.frame.size.height))
{
CGRect frame = self.a.frame;
frame.origin.y = presentationLayer.frame.origin.y - self.a.frame.size.height;
self.a.frame = frame;
}
}
You said that you tried the "animate B and use display link to update A" technique, and that it resulted in "A" lagging behind "B". You could theoretically animate a new view, "C", and then adjust B and A's frames accordingly in the display link, which should eliminate any lag (relative to each other).
You can try followin algorythm:
1)Put A and B in UIView(i.e UIview *gropedView)
2)Change B.y till it will be equal A.y+A.height(so B will be right under the A)
3)Animate groupedView

create particles without opengl and cocos2D. snowfall application

well I wanted to create particles like (snow ) without openGL or cocs2D, and I found this sample code called snowfall and in this code there is this :
flakeImage = [UIImage imageNamed:#"flake.png"];
// start a timet that will fire 20 times per second
[NSTimer scheduledTimerWithTimeInterval:(0.5) target:self selector:#selector(onTimer) userInfo:nil repeats:YES];
}
// Timer event is called whenever the timer fires
- (void)onTimer
{
// build a view from our flake image
UIImageView* flakeView = [[UIImageView alloc] initWithImage:flakeImage];
// use the random() function to randomize up our flake attributes
int startX = round(random() % 320);
int endX = round(random() % 320);
double scale = 1 / round(random() % 100) + 1.0;
double speed = 1 / round(random() % 100) + 1.0;
// set the flake start position
flakeView.frame = CGRectMake(startX, -100.0, 5.0 * scale, 5.0 * scale);
flakeView.alpha = 0.25;
// put the flake in our main view
[self.view addSubview:flakeView];
[UIView beginAnimations:nil context:flakeView];
// set up how fast the flake will fall
[UIView setAnimationDuration:5 * speed];
// set the postion where flake will move to
flakeView.frame = CGRectMake(endX, 500.0, 5.0 * scale, 5.0 * scale);
// set a stop callback so we can cleanup the flake when it reaches the
// end of its animation
[UIView setAnimationDidStopSelector:#selector(onAnimationComplete:finished:context:)];
[UIView setAnimationDelegate:self];
[UIView commitAnimations]; }
- (void)onAnimationComplete:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
UIImageView *flakeView = context;
[flakeView removeFromSuperview];
// open the debug log and you will see that all flakes have a retain count
// of 1 at this point so we know the release below will keep our memory
// usage in check
NSLog([NSString stringWithFormat:#"[flakeView retainCount] = %d", [flakeView retainCount]]);
[flakeView release];
}
I wanted to know if this code can hurt performance or memory ( maybe because it use a timer)?And if it is not a good idea I heard about CAReplicator that we can use for iphone but the CAReplicatorDemo only work for mac on the apple documentation :/ sorry for my english I'm french :/
Sure, it will affect performance. You will not see it in Simulator though. UIImageView is too heavy to be created 20 times a second. You may try to use a pool of CALayer-s instead (and CAAnimation).
You can use a CAEmitterLayer to do particle effects on iOS 5. I've used it for flames and smoke before but it should work just fine for snow.
You can try my KHParticleView, it use cocos2d particle and add cocos2d view on the top. Check the code here: https://github.com/lephukhanhhuy/KHParticleView

CGRect not changing during animation

I'm animating an UIView and want to check if its frame intersects with another UIView's frame. This is how I "spawn" one of the UIViews:
- (void) spawnOncomer
{
oncomer1 = [[Oncomer alloc] initWithType:#"car"];
[self.view addSubview:oncomer1];
//make the oncomer race across the screen
[UIView beginAnimations:nil context:NULL]; {
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:3.0];
[UIView setAnimationDelegate:self];
CGRect f = oncomer1.frame;
f.origin.y = self.view.frame.size.height+oncomer1.frame.size.height;
oncomer1.frame = f;
[UIView setAnimationDidStopSelector:#selector(decountCar)];
}
[UIView commitAnimations];
}
So far so good. Now I want to check if this UIView and my other UIView collide by doing this:
- (void) checkCollision {
bool collision = CGRectIntersectsRect(car.frame, oncomer1.frame);
if (collision) {
NSLog(#"BOOOOOOOM");
} else {
NSLog(#"Oncomer: %#", NSStringFromCGRect(oncomer1.frame));
}
}
However, they never collide. Although I see oncomer1 moving across the screen, loggin oncomer1.frame never changes: it keeps outputting Oncomer: {{50, 520}, {30, 60}}(which are the post-animation values).
Does anyone know why this is?
P.s. Both methods are called directly or indirectly with a NSTimer and are thus performed in the background
UIView geometry updates apply immediately to their CALayer, even in an animation block. To get a version of a layer with animations applied, you can use -[CALayer presentationLayer], like this (warning - untested code):
- (void) checkCollision {
CGRect oncomerFrame = oncomer1.layer.presentationLayer.frame;
bool collision = CGRectIntersectsRect(car.frame, oncomerFrame);
if (collision) {
NSLog(#"BOOOOOOOM");
} else {
NSLog(#"Oncomer: %#", NSStringFromCGRect(oncomerFrame));
}
}
From your code:
CGRect f = oncomer1.frame;
f.origin.y = self.view.frame.size.height+oncomer1.frame.size.height;
oncomer1.frame = f;
A logical explanation of the frame never changing is that you are only changing the y of the frame, and you are always setting it to the same value determined by two heights.

problem with collision of two images

Well here is my problem:
I have two images : flakeImage and ViewToRotate. What I want is that if flakeImage touches ViewToRotate, ViewToRotate.alpha=0.5; but when FlakeImage appears on the screen ViewToRotate.alpha=0.5; without touching it. I think it's a problem with my view beacause I have :
UIImageView* flakeView = [[UIImageView alloc] initWithImage:flakeImage];
here is the code :
UIImageView* flakeView = [[UIImageView alloc] initWithImage:flakeImage];
// use the random() function to randomize up our flake attributes
int startY = round(random() % 320);
// set the flake start position
flakeView.center = CGPointMake(490, startY);
flakeView.alpha = 1;
// put the flake in our main view
[self.view addSubview:flakeView];
[UIView beginAnimations:nil context:flakeView];
// set up how fast the flake will fall
[UIView setAnimationDuration:7 ];
// set the postion where flake will move to
flakeView.center = viewToRotate.center;
// set a stop callback so we can cleanup the flake when it reaches the
// end of its animation
[UIView setAnimationDelegate:self];
[UIView commitAnimations];
How can I solve this please ?
if someone could help me it would be very cool.
I have a bit of sophomoric experience with this, having written http://itunes.apple.com/us/app/balls/id372269039?mt=8. If you check that app out, you will see a bit of the same problem. This topic is a pretty deep rabbit hole. WHen I started that app, I didn't even know how to write a decent game loop. You will need that first because you need to do precise time-step calculations. AFA the collisions, you update your model and view separately, so if you update the model and objects overlap, you need to back them up until they don't collide and then update your view with the result. If you plan to have a lot of colliding objects, you may hit a wall using UIViews. To complicate things more, if your objects are round, CGRectIntersectsRect won't exactly work. That complicates the math a bit, but it's not too bad. With my app, I found it quite difficult to get the physics to look realistic. THe problem became that ball A and B overlap, so you back them up, then they now intersect other balls, etc, etc. This link is a good starting point, but there are quite a few examples of code out there that "almost" work.
CGRect has a intersection function. The frames of UIViews are CGRects.
if (CGRectIntersectsRect(view1.frame, view2.frame) == 1)
NSLog(#"The views intersect");
else
NSLog(#"The views do not intersect");
The problem I foresee is that if the rects have lots of whitespace, they will appear to intersect before they actually touch
Secondly, you should switch up to block animations. It's strongly encouraged
UIImageView* flakeView = [[[UIImageView alloc] initWithImage:flakeImage] autorelease];
// use the random() function to randomize up our flake attributes
int startY = round(random() % 320);
// set the flake start position
flakeView.center = CGPointMake(490, startY);
flakeView.alpha = 1;
// put the flake in our main view
[self.view addSubview:flakeView];
[UIView animateWithDuration:.7
animations:^ {
// set the postion where flake will move to
flakeView.center = viewToRotate.center;
};
Did this all from memory, no idea if there are errors.
Circular Collision:
a^2 + b^2 < c^2 means they collide
if(pow(view2.frame.origin.x - view1.frame.origin.x, 2) +
pow(view2.frame.origin.y - view1.frame.origin.y, 2) <
pow(view1.frame.size.width/2, 2))
{
//collision
}
else
{
//no collision
}
Again, all from memory, check for errors on your own

UIView scaling during animation

I've got a custom UIView to show a tiled image.
- (void)drawRect:(CGRect)rect
{
...
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClipToRect(context,
CGRectMake(0.0, 0.0, rect.size.width, rect.size.height));
CGContextDrawTiledImage(context, imageRect, imageRef);
...
}
Now I am trying to animate the resize of that view.
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:0.3];
// change frame of view
[UIView commitAnimations];
What I would expect is for the tiled area just to grow, leaving the tile sizes constant. What happens instead is that while the area grows the original content of the view is scaled to the new size. At least during the animation. So the at the beginning and the end of the animation all is good. During the animation the tiles are distorted.
Why is CA trying to scale? How can I prevent it from doing so? What did I miss?
If Core Animation had to call back to your code for every animation frame it would never be as fast as it is, and animation of custom properties has been a long requested feature and FAQ for CA on the Mac.
Using UIViewContentModeRedraw is on the right track, and is also the best you'll get from CA. The problem is from the UIKit point of view the frame only has two values: the value at the beginning of the transition and the value at the end of the transition, and that's what you're seeing. If you look at the Core Animation architecture documents you'll see how CA has a private representation of all layer properties and their values changing over time. That's where the frame interpolation is happening, and you can't be notified of changes to that as they happen.
So the only way is to use an NSTimer (or performSelector:withObject:afterDelay:) to change the view frame over time, the old fashioned way.
You might want to have a look at
http://www.nomadplanet.fr/2010/11/animate-calayer-custom-properties-with-coreanimation/
I was facing similar problem in a different context, I stumbled upon this thread and found Duncan's suggestion of setting frame in NSTimer. I implemented this code to achieve the same:
CGFloat kDurationForFullScreenAnimation = 1.0;
CGFloat kNumberOfSteps = 100.0; // (kDurationForFullScreenAnimation / kNumberOfSteps) will be the interval with which NSTimer will be triggered.
int gCurrentStep = 0;
-(void)animateFrameOfView:(UIView*)inView toNewFrame:(CGRect)inNewFrameRect withAnimationDuration:(NSTimeInterval)inAnimationDuration numberOfSteps:(int)inSteps
{
CGRect originalFrame = [inView frame];
CGFloat differenceInXOrigin = originalFrame.origin.x - inNewFrameRect.origin.x;
CGFloat differenceInYOrigin = originalFrame.origin.y - inNewFrameRect.origin.y;
CGFloat stepValueForXAxis = differenceInXOrigin / inSteps;
CGFloat stepValueForYAxis = differenceInYOrigin / inSteps;
CGFloat differenceInWidth = originalFrame.size.width - inNewFrameRect.size.width;
CGFloat differenceInHeight = originalFrame.size.height - inNewFrameRect.size.height;
CGFloat stepValueForWidth = differenceInWidth / inSteps;
CGFloat stepValueForHeight = differenceInHeight / inSteps;
gCurrentStep = 0;
NSArray *info = [NSArray arrayWithObjects: inView, [NSNumber numberWithInt:inSteps], [NSNumber numberWithFloat:stepValueForXAxis], [NSNumber numberWithFloat:stepValueForYAxis], [NSNumber numberWithFloat:stepValueForWidth], [NSNumber numberWithFloat:stepValueForHeight], nil];
NSTimer *aniTimer = [NSTimer timerWithTimeInterval:(kDurationForFullScreenAnimation / kNumberOfSteps)
target:self
selector:#selector(changeFrameWithAnimation:)
userInfo:info
repeats:YES];
[self setAnimationTimer:aniTimer];
[[NSRunLoop currentRunLoop] addTimer:aniTimer
forMode:NSDefaultRunLoopMode];
}
-(void)changeFrameWithAnimation:(NSTimer*)inTimer
{
NSArray *userInfo = (NSArray*)[inTimer userInfo];
UIView *inView = [userInfo objectAtIndex:0];
int totalNumberOfSteps = [(NSNumber*)[userInfo objectAtIndex:1] intValue];
if (gCurrentStep<totalNumberOfSteps)
{
CGFloat stepValueOfXAxis = [(NSNumber*)[userInfo objectAtIndex:2] floatValue];
CGFloat stepValueOfYAxis = [(NSNumber*)[userInfo objectAtIndex:3] floatValue];
CGFloat stepValueForWidth = [(NSNumber*)[userInfo objectAtIndex:4] floatValue];
CGFloat stepValueForHeight = [(NSNumber*)[userInfo objectAtIndex:5] floatValue];
CGRect currentFrame = [inView frame];
CGRect newFrame;
newFrame.origin.x = currentFrame.origin.x - stepValueOfXAxis;
newFrame.origin.y = currentFrame.origin.y - stepValueOfYAxis;
newFrame.size.width = currentFrame.size.width - stepValueForWidth;
newFrame.size.height = currentFrame.size.height - stepValueForHeight;
[inView setFrame:newFrame];
gCurrentStep++;
}
else
{
[[self animationTimer] invalidate];
}
}
I found out that it takes a long time to set the frame and the timer to complete its operation. It probably depends on how deep the view hierarchy is on which you call -setFrame using this approach. And probably this is the reason why the view is first re-sized and then animated to the origin in the sdk. Or perhaps, is there some problem in the timer mechanism in my code due to which the performance has hindered?
It works, but it is very slow, maybe because my view hierarchy is too deep.
As Duncan indicates, Core Animation won't redraw your UIView's layer's content every frame as it is resized. You need to do that yourself using a timer.
The guys at Omni posted a nice example of how to do animations based on your own custom properties that might be applicable to your case. That example, along with an explanation for how it works, can be found here.
If it is not a big animation or performance isn't an issue for the animation duration, you can use a CADisplayLink. It does smooth out the animation of the scaling of a custom UIView drawing UIBezierPaths for instance. Below is a sample code you can adapt for your image.
The ivars of my custom view contains displayLink as a CADisplayLink, and two CGRects: toFrame and fromFrame, as well as duration and startTime.
- (void)yourAnimationMethodCall
{
toFrame = <get the destination frame>
fromFrame = self.frame; // current frame.
[displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; // just in case
displayLink = [CADisplayLink displayLinkWithTarget:self selector:#selector(animateFrame:)];
startTime = CACurrentMediaTime();
duration = 0.25;
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)animateFrame:(CADisplayLink *)link
{
CGFloat dt = ([link timestamp] - startTime) / duration;
if (dt >= 1.0) {
self.frame = toFrame;
[displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
displayLink = nil;
return;
}
CGRect f = toFrame;
f.size.height = (toFrame.size.height - fromFrame.size.height) * dt + fromFrame.size.height;
self.frame = f;
}
Note that I haven't tested its performance, but it does work smoothly.
I stumbled upon this question when trying to animate a size change on a UIView with a border. I hope that others who similarly arrive here might benefit from this answer. Originally I was creating a custom UIView subclass and overriding the drawRect method as follows:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 2.0f);
CGContextSetStrokeColorWithColor(ctx, [UIColor orangeColor].CGColor);
CGContextStrokeRect(ctx, rect);
[super drawRect:rect];
}
This led to scaling problems when combining with animations as others have mentioned. The top and bottom of the border would grow too thick or two thin and appear scaled. This effect is easily noticeable when toggling on Slow Animations.
The solution was to abandon the custom subclass and instead use this approach:
[_borderView.layer setBorderWidth:2.0f];
[_borderView.layer setBorderColor:[UIColor orangeColor].CGColor];
This fixed the issue with scaling a border on a UIView during animations.