I am using transitionFromView with UIViewAnimationOptionTransitionFlipFromLeft. Now I need to combine number of such transitions in the way so I know the completion of whole transformation.
What is the best way to do that? Can I use something like animateWithDuration, then put multiple transitionFromView in animations block? If yes, then would duration params of both messages interfere with each other? What would mean each of those duration param in that case?
Is there better way to flip multiple views with completion block for all?
You can use a CATransaction to do this easily.
First, add the QuartzCore framework to your target, if you haven't already.
Next, add #import <QuartzCore/QuartzCore.h> to the top of your .m file, if you haven't already.
Execute [CATransaction begin] before the statements that create your transitions. Then set the completion block for the new transaction. Next, create the animations. Finally, commit the transaction. Here's an example:
- (IBAction)flipButtonWasTapped:(id)sender {
[CATransaction begin]; {
[CATransaction setCompletionBlock:^{
NSLog(#"all animations complete!");
}];
[UIView transitionFromView:self.topFrontLabel
toView:self.topBackLabel duration:1
options:UIViewAnimationOptionTransitionFlipFromRight
completion:nil];
[UIView transitionFromView:self.bottomFrontLabel
toView:self.bottomBackLabel duration:1.5
options:UIViewAnimationOptionTransitionFlipFromRight
completion:nil];
} [CATransaction commit];
}
Note that you must set the completion block before you create any animations. The completion block only waits for animations that are added after it is set.
Note also that all methods of CATransaction are class messages. You don't get an object representing the transaction.
Related
I am working with animations in iOS7 with objective-c. I am trying to use the animateWithDuration function which has the following definition:
[UIView animateWithDuration:(NSTimeInterval) animations:^(void)animations completion:^(BOOL finished)completion]
I can use this just fine, but it makes my code overly long because I have to put my animation and completion functions all in this declaration. I would like to create a separate function and pass it into the animation function call.
Specifically I would like to be able to have a separate completion function to use with multiple animations, which would also require the ability to pass it the parameter of a the specific view's id.
Could someone explain how to set up a function that can be passed into the animate function, and also what the '^' in the ^(void) and ^(BOOL) means?
Thanks
That ^ indicates a block (note that these are not functions). You can certainly do what you want. You would use:
returnType (^blockName)(parameterTypes) = ^returnType(parameters) {...};
So your code would look something like this:
void (^animations)() = ^{
// Do some animations.
};
void (^completion)(BOOL) = ^(BOOL finished){
// Complete.
};
[UIView animateWithDuration:1 animations:animations completion:completion];
FYI, this is a great reference for block syntax: http://goshdarnblocksyntax.com/
Don't overcomplicate things. Just use this method instead:
[UIView animateWithDuration:1.0 animations:^{
// your animations
}];
Next time you come across a block you have no use for, just put nil in the block.
[UIView animateWithDuration:1.0
animations:^{
// your animations
}
completion:nil];
The ^ means that you are declaring a block in Objective-C.
If you just want to make your method call shorter, you can do this:
void (^myCompletionBlock)(BOOL finished) = ^void(BOOL finished) {
// What you want to do on completion
};
[UIView animateWithDuration:1.0
animations:^{
// your animations
}
completion:myCompletionBlock];
I want to call a method after an MKMapView animates to a new MKMapCamera.
I started out by attaching the MKMapCamera using this method:
[self.map setCamera:cam animated:YES];
This method causes the animation but doesn't inform me when the animation finished.
I then tried implementing a callback method by using UIView animation blocks after seeing this SO post:
MKMapCamera *cam = [[MKMapCamera alloc] init];
cam.pitch = 75;
cam.altitude = 125;
[cam setCenterCoordinate:self.location.coordinate];
[UIView animateWithDuration:3.0f animations:^{
self.map.camera = cam;
} completion:^(BOOL finished) {
[self methodToImplement];
}];
The MKMapView still animates, however the methodToImplement is called at the same time.
Thanks!
It appears that the completion handler isn't working right for that case, so you'll need to use the MKMapViewDelegate method for region did change. See WWDC 2013 - Putting Map Kit in Perspective:
Okay now that I fired off this animation to go to the next camera I need to know when that animation completes so that I can then animate to the next camera in our stack.
Well you might think of using the completion handler here but it's going to trip you up.
I know it will.
Don't use that completion handler.
Instead you need to use MKMapViews delegate method which tells you when a region change is completed.
If you're not going to use a lot of different kind of animations, then you'll might be ok with simply using the mapView:regionDidChangeAnimated: method and checking the animated flag to call your 'methodToImplement' (the animated flag will only be true for region change that was due to animation calls, and not user input like dragging the map).
In my own project I have a more complex flow, so I needed a more flexible solution, so I opted to adding an NSMutableArray of NSBlockOperation objects as a property in my view controller containing the MKMapView. Each block operation corresponding to a would be completion handler that we can't use.
In mapView:regionDidChangeAnimated: I just pop the operations one by one and execute them:
- (void)mapView:(MKMapView *)mapView regionDidChangeAnimated:(BOOL)animated {
if (animated) {
dispatch_async(dispatch_get_main_queue(), ^{
while (self.mapRegionAnimationBlocks.count > 0) {
NSBlockOperation* op = [self.mapRegionAnimationBlocks firstObject];
[self.mapRegionAnimationBlocks removeObjectAtIndex:0];
[op start];
}
});
});
and where ever I want to use a completion handler for that block, I just add to that array before calling the animation code:
dispatch_async(dispatch_get_main_queue(), ^{
NSBlockOperation* op = [NSBlockOperation blockOperationWithBlock:^{
[self methodToImplement];
}];
[self.mapRegionAnimationBlocks addObject:op];
});
[UIView animateWithDuration:3.0f animations:^{
self.map.camera = cam;
} completion:NULL];
Note that it's important to use NSMutableArray only from a single thread (e.g. the main thread), because it's not thread-safe.
My solution is a bit of hack, and one which should probably be wrapped in a category or subclass of MKMapView, but I haven't gotten around to that yet.
If you set your map view's delegate, you can then write a mapViewDidFinishLoadingMap:, which "Tells the delegate that the specified map view successfully loaded the needed map data."
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView
{
// do whatever you want
}
If you want to know not only when the map data is loaded, but also when the rendering of the map is complete (in iOS7+), you can use mapViewDidFinishRenderingMap, which "Tells the delegate that the map view has finished rendering all visible tiles."
- (void)mapViewDidFinishRenderingMap:(MKMapView *)mapView fullyRendered:(BOOL)fullyRendered
{
// do whatever you want
}
For more information, see MKMapViewDelegate Protocol Reference.
In my project, the program fire an animation when querying the database, when it receives the response data, it fires another animation automatically to display the data.
-(void)queryDatabase{
//...querying database ...
[UIView animateWithDuration:0.4
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
//...first animation block...
}
completion:^(BOOL finished){
//...first completion block...
}];
}
-(void)receivedResponse:(NSData *)responseData{
[UIView animateWithDuration:0.4
delay:0
options:UIViewAnimationOptionBeginFromCurrentState
animations:^{
//...second animation block...
}
completion:^(BOOL finished){
//...second completion block...
}];
}
My problem is, when the program is initiated and fire the second animation at the first time received response, the "second animation block" is exactly executed, but the "second completion block" is not executed and the screen does not change until about 20 or more seconds passed. After that, when this cycle is invoked again, the second animation will always work correctly. How to fix?
Firstly, all UI code should execute on the main thread. You will get unexpected results if you don't abide by this. I've experienced nothing happening at all when I want some UI code to run, and apps hanging whenever I've mistakenly run UI code on a background thread. There are plenty of discussions on why UI code has to run on the main thread.
If you know you're receivedResponse method is going to execute on a thread other than the main thread, you can easily bring it back to the main thread using Grand Central Dispatch (GCD). GCD is a lot nicer to use than the performSelectorOnMainThread methods...
-(void)receivedResponse:(NSData *)responseData{
dispatch_async(dispatch_get_main_queue(), ^{
// put your UI code in here
});
}
A user can initiate an animation with a swipe gesture. I want to block duplicate calls to the animation, to make sure that once the animation has started, it cannot be initiated again until it has completed -- which may happen if the user accidentally swipes multiple times.
I imagine that most people achieve this control using a boolean flag (BOOL isAnimatingFlag) in the manner shown at bottom. I've done things like this before in apps many times -- but I never feel 100% certain as to whether my flag is guaranteed to have the value I intend, since the animation uses blocks and it's unclear to me what thread my animation completion block is being run on.
Is this way (of blocking duplicate animations) reliable for multi-thread execution?
/* 'atomic' doesn't
* guarantee thread safety
* I've set up my flag as follows:
* Does this look correct for the intended usage?
*/
#property (assign, nonatomic) BOOL IsAnimatingFlag;
//…
#synthesize IsAnimatingFlag
//…
-(void)startTheAnimation{
// (1) return if IsAnimatingFlag is true
if(self.IsAnimatingFlag == YES)return;
/* (2) set IsAnimatingFlag to true
* my intention is to prevent duplicate animations
* which may be caused by an unwanted double-tap
*/
self.etiIsAnimating = YES;
// (3) start a new animation
[UIView animateWithDuration:0.75 delay:0.0 options:nil animations:^{
// animations would happen here...
} completion:^(BOOL finished) {
// (4) reset the flag to enable further animations
self.IsAnimatingFlag = NO;
}];
}
Disable the gesture if you don't want the user triggering it multiple times
- (void)startTheAnimation:(id)sender
{
[sender setEnabled:NO];
[UIView animateWithDuration:0.75 delay:0.0 options:nil animations:^{
// animations would happen here...
} completion:^(BOOL finished) {
[sender setEnabled:YES];
}];
}
Update
Gestures also have an enabled property so you could use the same idea as if it were a button and change it' enabled state
Animation completion block will always run on the main thread.
In the example in the UIView Class Reference you can see that [view removeFromSuperview] is called directly from the block. That's mean a completion block runs on the main thread as it's the only thread safe to call UI-releated methods.
So you are all good if you calling startTheAnimation only from the main thread. If you not you need to dispatch it on the main thread anyway because you call UI-releated methods in it.
If you need to call startTheAnimation from other threads than main thread you can do something like this:
-(void)startTheAnimation{
dispatch_async(dispatch_get_main_queue(), ^{
// Your code here
});
}
Of course, it's better from user experience point of view to, for example, disable a button or modify the UI in other ways to indicate that an animation is in progress. However, it's all the same code. Whatever you need to do you first need to disable it before an animation starts and the re-enable after it's finished.
Where you call this method, you could try using dispatch_once GCD function:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[myThingy startTheAnimation];
});
I want to perform an animation of the position of some CALayers. Before the end of the animation, I want to push another UIViewController so that when I pop this last UIView controller, the CALayers are back in their original positions. This is my code:
CABasicAnimation *animation4 = [CABasicAnimation animationWithKeyPath:#"position"];
animation4.fromValue = [control.layer valueForKey:#"position"];
CGPoint endPoint4=CGPointMake(512, -305);
animation4.toValue =[NSValue valueWithCGPoint:endPoint4];
animation4.duration=1;
[control.layer addAnimation:animation4 forKey:#"position"];
[self performSelector:#selector(goToSolutionViewController) withObject:nil afterDelay:0.9];
And in goToSolutionViewController I have:
-(void)goToSolutionViewController{
SolutionViewController *solution=[self.storyboard instantiateViewControllerWithIdentifier:#"SolutionViewID"];
[self.navigationController pushViewController:solution animated:NO];
}
The problem is that
[self performSelector:#selector(goToSolutionViewController) withObject:nil afterDelay:0.9]
is not called until the end of the animation. So goToSolutionViewController is called after 1.9 seconds instead of 0.9.
What can I do to push the UIViewController before the animation ends? Or to have the CALayers back in the original position when I pop the UIViewController but the user can't see the way back.
Edit: ---
This performance issue only happens the first time I perform the animation and push the UIViewcontroller. When I pop it and do everything again, the performance is as specter. The problem might be with the UIViewController load time the first time.
Instead of relying on the timing of the animation compared to performing after a delay you should use one of the animation callbacks to call your method. You can either use a CATransaction, which will allow you to use a block, or the normal delegate methods.
Using a CATransaction
By wrapping your animation (adding it to the layer) in a transaction you can use the completion block of the transaction.
[CATransaction begin];
// Your animation here...
[CATransaction setCompletionBlock:^{
// Completion here...
}];
[CATransaction commit];
Using the delegate callbacks
By setting yourself as the animations delegate you get a delegate callback when the animation finishes.
animation4.delegate = self;
And the callback
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
// Completion here..
}