UIView animation to "rewind" a pan gesture: recursion? - objective-c

I have a scenario where the user dragged a UIView somewhere; I want to return it to where it started, and I want it to return along the same track and at the same speed that the user dragged. I have come up with a recursive solution that looks like this:
#property (...) NSMutableArray *dragTrack; // recorded during pan gesture
- (void)goHome
{
[self backtrack: [self.dragTrack count]
withDelay: 1.0 / [self.dragTrack count]];
// interim version of delay; will implement accurate timing later
}
- (void)backtrack: (int)idx withDelay: (double)delay
{
if (idx > 0){
[UIView animateWithDuration:0 delay:delay options: 0
animations: ^{
self.center = [[self.dragTrack objectAtIndex:idx - 1] CGPointValue];
}
completion:^(BOOL finished){
[self backtrack: idx - 1 withDelay: delay];
}];
} else {
// do cleanup stuff
}
}
This works, and recursion depth doesn't appear to be an issue - my drag tracks are typically only a couple of hundred points long. (I assume the chances of the recursive call to backtrack getting tail call optimized are rather slim?). But I'm still wondering: is this a reasonable/normal/safe solution to what I'm trying to achieve? Or is there a simpler way to pass a collection of timestamps and states to an animation and say "play these"?

You are not causing recursion there. Since the completion block is called asynchronously, it is not getting deeper into function stack. You can see it yourself by setting a breakpoint there and check the stack trace.
You can imagine NSTimer there triggering theoretically infinite times and not causing recursion.
To your second question, you can make such animation. It is called CAKeyframeAnimation and it is part of QuartzCore framework. It would require you to work with layers instead of views, but it is not that difficult.
Just one note to your code: try to use option UIViewAnimationOptionCurveLinear for the animation to be more smooth.

Related

Can I 'flush' UIGestureRecognizer events?

In iOS 7, Apple seems to have changed the way the gesture recognizers behave. Consider UIPinchGestureRecognizer as an example. If I do a slow redrawing operation in UIGestureRecognizerStateChanged, this used to work fine under old versions of iOS, but in newer versions, my redrawing typically doesn't get rendered to the screen before the pinch gesture is called again with another StateChanged update, and the slow drawing operation is invoked again. This happens repeatedly many times before the system actually updates the visible portion of the screen with my changes to the views.
I've found one solution is to call:
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
whenever I get a UIGestureRecognizerStateChanged event. This way drawing gets rendered on the screen, each time it's done. But there is still an issue of "event lag" where a series of pinch events gets queued up, such that the images keep scaling in size even long after I've stopped pinching the screen.
My question is if there's a way to "flush" the queued up pinch events, so whenever I get a UIGestureRecognizerStateChanged event, I can do my slow drawing operation, then flush all other pinch events, so only the most recent one gets processed. Anyone know if this is possible? I guess I could build a system that looks at the time of a UIGestureRecognizerStateChanged event, and throws out events too close to the most recent redraw, but that seems like a hack.
- (void) handleGlobalPinchGesture:(UIPinchGestureRecognizer*)_pinchGesture
{
if ( _pinchGesture.state == UIGestureRecognizerStateBegan )
{
// stuff
return;
}
if ( _pinchGesture.state == UIGestureRecognizerStateEnded || _pinchGesture.state == UIGestureRecognizerStateCancelled )
{
// end stuff
return;
}
if (_pinchGesture.state == UIGestureRecognizerStateChanged )
{
doSlowRedrawingOperationHere();
}
}
I do not think, that it is the gesture recogniser's problem, I've had same problem with moving a transformed view. And I've solved it, by add to view drawRect method, and call -(void)setNeedsDisplay method before change the centre of the view:
In view:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
}
In a gesture recogniser's action:
[CATransaction begin];
[CATransaction setValue:#(YES) forKey:kCATransactionDisableActions];
_destinationIndicatorView.center = center;
[self.frameView setNeedsDisplay];
self.frameView.center = center;
[CATransaction commit];
It works for me.
I never did find a way to 'flush' these events, but I did find a 'hack' that ensures every render is reflected on-screen, so the user sees your gesture actions in real-time, even if such redrawing operations are slow. My solution is to call:
[[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate date]];
to 'give the OS time' to do the redrawing on-screen. I do this from within my gesture recognizer callback, and only if running on iOS 7 or later.
The above call must be added to the callback for all your gesture recognizers (after the new content is rendered). Hope this helps someone!
Too bad that this 'hack' currently seems to be required in iOS 7 when you have slow rendering that takes place as a direct response to a gesture, if you want a good user experience.

Detecting if a Cocoa Animation is in Progress (Using BOOL) to prevent multiple simultaneous animations

In my app, I run numerous animations, moving objects and shapes etc.
Many of the animations lead on to the animation of other objects.
A problem with the application is that if an animation is running and you start another that involves one of the objects currently being animated, the screen goes white/we get unexpected results.
I worked up some sample code to show what we are trying to do:
UIButton *button;
BOOL currentlyShowingQuestion;
- (void)viewDidLoad
{
[self moveButton:button];
currentlyShowingQuestion = NO;
[NSTimer scheduledTimerWithTimeInterval:.1 target:self selector:#selector(hasBoolChanged) userInfo:nil repeats:YES];
}
- (void)moveButton:(UIButton *)buttonToUse {
currentlyShowingQuestion = YES;
NSLog(#"moveButton Called! Yippee!!!!!!!");
[UIView animateWithDuration:0.5 delay:0.5 options:nil animations:^{
buttonToUse.frame = CGRectMake(10, 10, 300, 440);
} completion:^(BOOL finished){
currentlyShowingQuestion = NO;
}];
}
-(void)hasBoolChanged {
if (currentlyShowingQuestion == YES) {
NSLog(#"Yes");
}
else if (currentlyShowingQuestion == NO) {
NSLog(#"No");
}
}
What I end up actually seeing is like the currentlyShowingQuestion BOOL both get changed instantly when the method is run, so it changes to YES and instantly back to NO. Then the animation runs. Why is this? What's wrong with the order?
Any help appreciated.
Sorry about the code formatting - I couldn't figure out the syntax here.
Thanks in advance,
Sam
I don't really understand what you try to achieve. At the beginning you start a timer that checks whether the value of the boolean has changed. I think it is kind of an overkill for this task but thats ok.
Your animation is delayed 0.5s for some reason and then you have a 0.5s duration for an animation and that is really fast so it could seem that you instantly change the value.
I'm not too familiar with UIView, but according to the docs, the completion block may be called before the animation's finished. Try add a condition whether finished is YES before you change currentlyShowingQuestion to NO.

Checking When animation stopped

I am working on a simple UIImageView animation. Basically I got a bunch of image numbers from 0-9 and I am animating them using this code.
myAnimation.animationImages = myArray;
myAnimation.animationDuration = 0.7;
myAnimation.animationRepeatCount = 12;
[myAnimation startAnimating];
Here myArray is an array of uiimages i.e. 0.png, 1.png etc.
All is well and its animating just fine. What I need to know is when do I know when the animation has stopped? I could use NSTimer but for that I need a stopwatch to check when the animation starts and stops. Is there a better approach, meaning is there a way I can find out when the UIImageView stopped animating?
I looked at this thread as reference.
UIImageView startAnimating: How do you get it to stop on the last image in the series?
Use the animationDidStopSelector. This will fire when the animation is done:
[UIView setAnimationDidStopSelector:#selector(someMethod:finished:context:)];
then implement the selector:
- (void)someMethod:(NSString*)animationID finished:(NSNumber*)finished context:(void*)context {
}
Yes, there is a much better approach than using NSTimers. If you're using iOS 4 or higher, it is better you start using block animations. try this
[UIView animateWithDuration:(your duration) delay:(your delay) options:UIViewAnimationCurveEaseInOut animations:^{
// here you write the animations you want
} completion:^(BOOL finished) {
//anything that should happen after the animations are over
}];
Edit: oops I think I read your question wrong. In the case of UIImageView animations, I can't think of a better way than using NSTimers or scheduled events
You can also try this:
[self performSelector:#selector(animationDone) withObject:nil afterDelay:2.0];

UIView animations in a loop

Is there a reason why animations do not work in a loop? I know you can do the command:
[UIView setAnimationRepeatCount: someNumber];
which was working fine until I put actual different pdfs to be loaded. So before, with just one pdf that would always get loaded, when I did the animation say 5 times to go to the last document, the animations would look correct. But now that I have 5 different pdfs, it will flip the page 5 times with the CurlUp but it will keep showing the 1st pdf in the list, and then at the end load the proper one. So I thought I could do it in a loop, keeping track of the location in the array, and load each pdf at each location as i flip through the pages so it gives the user the feeling of where they are in the stack of pdfs. But when I put the core animation code in a loop, it basically shows the animation once, but does get to the correct document.
From my understanding of what you're doing, it sounds like you're actually running all 5 animations at once. When you perform an animation block in a for-loop, the runloop doesn't iterate between each loop and only the last animation gets run.
What you'll need to use is use completion calls and "chain" them together. So you run the first animation, then on that completion call you run the second, then on that call the third, etc. Much more lengthy to code, but this will work.
Would be easier to use blocks in this scenario, then all your animation code will be in one place instead of having to spread it all across a bunch of methods for each animation. You would just nest the blocks, in the first animation's completion block you'd spawn the second, then the third, etc.
Here's an example using the blocks method. It's a little hard to read, but you'll get the idea. You might be able to create a single animation block and reuse it in each completion method, but I don't know all of the catches of doing that, and this is much easier to read when you're first trying to understand it.
Note that I haven't actually tried this or tested it, but I think it will do what you want.
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:^(BOOL finished) {
// Start next animation which will run with this one finishes
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:^(BOOL finished) {
// Start next animation which will run with this one finishes
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:^(BOOL finished) {
// Start next animation which will run with this one finishes
[UIView animateWithDuration:1.0 animations:^(void) {
// Animation changes go here
// blah.alpha = 1.0
// blah.position = CGPointMake
// etc
} completion:nil];
}];
}];
}];

Animated UIImageView which should recognize taps on it (at every location the UIImageView is at)

I'm making an iPhone App which involves balloons (UIImageViews) being spawned from the bottom of the screen and moving up to the top of the screen.
The aim is to have the UIImageViews set up so that a tap gesture on a balloon causes the balloons to be removed from the screen.
I have set up the GestureRecognizer and taps are being recognized.
The problem I am having is that the animation method I am using does not provide information about where the balloon is being drawn at a specific point in time, and treats it as already having reached the final point mentioned.
This causes the problem of the taps being recognized only at the top of the screen(which is the balloon's final destination).
I have pasted my code for animation here:
UIViewAnimationOptions options = UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionAllowUserInteraction;
[UIView animateWithDuration:2.0 delay:0.0 options:options animations:^{
balloonImageView.center = p;
} completion:^(BOOL finished) {
if (finished) {
balloonImageView.hidden= TRUE;
balloonImageView.image=nil;
}
I keep track of where an image is by giving a unique tag to a UIImageView and checking if gesture.view.tag = anyBalloonObject.tag and if it does I destroy the balloon.
My basic question is, is there another animation tool I can use? I understand that I can set a balloon's finish point to slightly higher than where it is on screen until it reaches the top but that seems like it is very taxing on memory.
Do you have any recommendations? I looked through the Core Animation reference, and didn't find any methods that suited my needs.
In addition will the Multi-threading aspects of UIViewAnimation affect the accuracy of the tap system?
Thank You,
Vik
You shouldnt animate game-objects (objects you interact with while they are moving) like that.
You should rather use a NSTimer to move the ballons.
-(void)viewDidLoad {
//Set up a timer to run the update-function
timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:#selector(update) userInfo:NULL repeats:YES];
}
-(void) update {
for (int i = [balloonArray count] - 1; i >= 0; i--) { //Loop through an array containing all your balloons.
UIImageView *aBalloon = [balloonArray objectAtIndex:i]; //Select balloon at the current loop index
aBallon.center = CGPointMake(aBallon.center.x, aBalloon.center.y + 1); //Change the center of that balloon, in this case: make it go upwards.
}
}
Here is what you need to declare in .h:
NSTimer *timer;
NSMutableArray *ballonArray;
//You should use properties on these also..
You will have to create a spawn method which adds balloons to the screen AND to the array.
When you have done that, it's simple to implement the touch methods, You can just loop trough the balloons and check:
//You need an excact copy of the loop from earlier around this if statement!
If (CGRectContainsPoint(aBalloon.frame, touchlocation)) { //you can do your own tap detection here inthe if-statement
//pop the balloon and remove it from the array!
}
Hope that isnt too scary-looking, good luck.