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.
Related
I have a View to show and hide to give users some hint.
The show and hide method look some like this:
-(void)show{
[UIView animateWithDuration:3.0f
animations:^{
//do something to show self to give hint;
self.frame = CGRectMake(0,0,0,0);
} completion:nil];
}
-(void)hide{
[UIView animateWithDuration:3.0f
animations:^{
//do something to hide self to give hint;
self.frame = CGRectMake(centerX,centerY,100,100);
} completion:nil];
}
when showing a new view, I must call hide method, and then show method. But the duration delay, 3.0f, will cause some error. I was using methods like this:
dispatch_async(dispatch_get_main_queue(), ^{
[view hide];
});
dispatch_async(dispatch_get_main_queue(), ^{
[view show];
});
I was calling show method right after hide method. Animations cannot execute as the sequence they are submitted to the queue. What I want is the show method executed exactly after the hide method completed. How can I control the order of these two methods.
I think I cannot use the completion handler cause I cannot assure where these two methods are called, or whether the view is shown when I called another show method or hide method.
If I am not clear, any suggestions? I will reedit my questions.
PS:
It's not just a flash. When next show method is called, I can not assure the last view is shown or hide and how long the last view is being shown, that is, if the view is being shown and the hide method has been called and already completed, then the show method is called, the result is right. If the view is being shown, another hint view need to be presented, I will call hide first, then show, since the main_queue is serial but the animation block is executed syncly, so the result is wrong. I am looking for is there some kind of lock in GCD that can help me execute a block after last queued block is completed rather than changing within show and hide method. cause there are many other calls to show and hide method with many different kinds of parameters, I need to fix many places in my code.
If you want to execute one task at a time in the order in which they are added to the queue, Use serial queue.
So you can use a serial queue to execute show and hide task at a time in the added order. Yeah, the main queue is ok for that.
However UIView -animateWithDuration:animations: method is kind of asynchronous call, the method returns immediately. So you need to wait until the completion block was called.
If you want to wait until some tasks were finished, Use dispatch group. But you should avoid to wait like that on the main queue. It blocks the main queue. Bad app.
Thus, you might need to use a serial queue and dispatch group as the following.
properties and initialize
#property (nonatomic, strong) dispatch_queue_t serialQueue;
#property (nonatomic, strong) dispatch_group_t group;
-(void)initQueue {
// create a serial queue
self.serialQueue = dispatch_queue_create("com.example.serialQueue", 0);
// create a dispatch group
self.group = dispatch_group_create();
}
a method that uses the serial queue and the dispatch group
-(void)animateSyncWithDuration:(NSTimeInterval)duration animations:(block_t)animations {
dispatch_async(self.serialQueue, ^{
/*
* This block is invoked on the serial queue
* This block would never be executed concurrently
*/
/*
* Enter the dispatch group
*/
dispatch_group_enter(self.group);
dispatch_async(dispatch_get_main_queue(), ^{
/*
* This block is invoked on the main queue
* It is safe to use UIKit
*/
[UIView animateWithDuration:duration animations:animations completion:^{
/*
* This completion block is invoked on the main queue
* Now leave the dispatch group
*/
dispatch_group_leave(self.group);
}];
});
/*
* Wait until leaving the dispatch group from the UIView animation completion block
* It means it blocks the serial queue
*/
dispatch_group_wait(self.group, DISPATCH_TIME_FOREVER);
});
}
show and hide
-(void)show{
[self animateSyncWithDuration:3.0f animations:^{
//do something to show self to give hint;
self.frame = CGRectMake(0,0,0,0);
}];
}
-(void)hide{
[self animateSyncWithDuration:3.0f animations:^{
//do something to hide self to give hint;
self.frame = CGRectMake(centerX,centerY,100,100);
}];
}
If what you wanted is one action (hide then show itself), you should make just one animation to do this instead of join two animations.
There are two possible solutions.
(1) use animation repeat and auto-reverse (need to reset back to original size in completion callback)
-(void) flash {
CGRect bounds = self.bounds;
[UIView animateWithDuration:1.0f
delay:0.0f
options:UIViewAnimationOptionAutoreverse |
UIViewAnimationOptionRepeat
animations:^{
[UIView setAnimationRepeatCount:1];
self.bounds = CGRectZero;
}
completion:^(BOOL finished) {
self.bounds = bounds;
}];
}
(2) use key frame animation
-(void) flash2 {
[UIView animateKeyframesWithDuration:1.0f
delay:0.0f
options:UIViewKeyframeAnimationOptionCalculationModeLinear
animations:^{
CGRect bounds = self.bounds;
[UIView addKeyframeWithRelativeStartTime:0.0
relativeDuration:0.5
animations:^{ self.bounds = CGRectZero; }];
[UIView addKeyframeWithRelativeStartTime:0.5
relativeDuration:0.5
animations:^{ self.bounds = bounds; }];
}
completion:nil];
}
i am using following way to put delay in function calling.
- (void) doAnimation : (double) delay {
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"Call your First function here");
});
double delayInSeconds = delay;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
NSLog(#"Call your second function here");
});
}
I may not be fully understanding the use case, but what I think you should be doing here is checking whether the hide operation actually needs to occur. Also, since the hide has an animation duration of 3 seconds in your code, you should create your method with a completion block, so you can do something similar to what I've written below in pseudocode:
- (void)hideIfNeededWithCompletionBlock:((^)())completionBlock {
if (self.isShowing) {
[self hideWithCompletionBlock:^(BOOL didHide) {
if (completionBlock) {
completionBlock();
}
}];
} else {
if (completionBlock) {
//We didn't need to hide anything, so we're done
completionBlock();
}
}
}
Then you can call it like so:
[self hideIfNeededWithCompletionBlock:^(){
[self show];
}];
You can do something similar with the show method if you need that flexibility.
Also, depending on your needs, you can make your method take a BOOL for whether to animate the show/hide and if you pass NO, use a duration of 0.0.
I think its working against the UIView animations API to start wrapping it in dispatch_async blocks when you can handle it all with the API provided.
I am running an animation on the iPhone with the below recursive function. When I animate, user interaction is blocked (and when the animation is done, user interaction works). I have been trying to enable user interaction, and have tried
passing the flag UIViewAnimationOptionAllowUserInteraction to animateWithDuration.
defining a function called touchesBegan as of this website. This function is never called (and called in other views when I tap the screen).
running the animation on a different thread with dispatch_async and dispatch_sync as this SO answer specifies. I have tried several methods but am not even sure if it'll work.
putting a UIButton in to detect taps. The function it's linked to isn't called for ~1-2 seconds.
To me, that all sounds like user interaction isn't enabled. How can it be responsive while this animation is running?
This animation is rather long and complex -- it's the whole reason this app exists. Each longAndComplicatedCalculation takes about 1s and this function is called ~30 times.
- (void)startAnimation:(dispatch_block_t)block withUIBlock:(dispatch_block_t)uiBlock iteration:(int)N{
[UIView animateWithDuration:1.0
delay:0.0
options:(UIViewAnimationOptionCurveLinear | UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionAllowAnimatedContent)
animations:^(void) {
[block invoke];
[uiBlock invoke];
}
completion:^(BOOL finished) {
if(FINISHED_IF && N<N_MAX) {
__weak id weakSelf = self;
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[weakSelf startAnimation:block withUIBlock:uiBlock iteration:N+1];
}];
}
}
];
}
This function is called with
[self startAnimation:^{
imageChange = [self longAndComplicatedCalculation];
} withUIBlock:^{
self.imageView.image = imageChange;
}
iteration:1];
You are calling both blocks (the block and the uiBlock) from the main thread. If your longAndComplicatedCalculation is blocking the thread the behaviour is normal. You should call your calculation in a separate thread and from there after finishing call the UIThread to initiate the animation.
To make it more clear, this is the way I would do it (without having your exact implementation and testing):
considering your image is declared as property:
#property (nonatomic, retain) UIImage *changedImage;
when you call the animation you can do it like
[NSThread detachNewThreadSelector:#selector(updateImage) toTarget:nil withObject:nil];
and in the function you do the calculation and afterwards call the animation on the UIThread:
- (void)updateImage {
self.changedImage = [self longAndComplicatedCalculation];
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self doAnimationAndChangeImage];
}];
}
I hope this helps.
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 insert UIViews dynamically with animation, based on incoming Data.
I send two asynchronous URLRequests and get the received data back from another thread.
I'm observing a property, which gets dynamically filled with data from different Requests. So KVO sends me messages, probably directly after each other.
With the first incoming message, I start a few dependent animations. During the animation time it often occurs, that new data arrives and messages my observer. I then want to wait until the animations are over and start the animations for the new data.
I'm looking for a nice implementation, not something with a while loop, waiting until a bool changes.
EDIT:
Consider that I cannot say, which data comes in first. So I cannot define a animation method as the first one.
How about adding the incoming views to a mutableArray as they come in and then use something like this:
-(void) animateView:(UIView *)viewToAnimate{
[UIView animateWithDuration:0.5 animations:^{
viewToAnimate.alpha = 0;
}
completion:^(BOOL finished){
[_mutableArrayOfAnimationViews removeObject: viewToAnimate];
if ([_mutableArrayOfAnimationViews count] > 0) {
UIView *newAnimationView = [_mutableArrayOfAnimationViews objectAtIndex:0];
[self animateView: newAnimationView];
}
}];
}
Then you can also check when the views come in whether the array is already empty, and if so call the above method...
How about create a mutable array that's your kvoToDoList for animations. Put whatever info in there you need to launch the animation (like the object that got the kvo triggered).
Then when the kvo is observed, add the object to the array and call an animation function like this:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
self.kvoToDoList insertObject:object atIndex:0];
// not sure here what you need to remember in order to do the animation, let's say it's just the object
[self doAnimations];
}
- (void)doAnimations {
if (self.kvoToDoList.count == 0) return;
id object = [self.kvoToDoList lastObject]; // FIFO since we insert at index 0
[self.kvoToDoList removeLastObject];
[UIView animateWithDuration:someDuration delay:0.0 options:someOptions animations:^{
// do animations here based on object
} completion:^(BOOL finished) {
// call this recursively, but performSelector so we don't wind up the stack
[self performSelector:#selector(doAnimations) withObject:nil afterDelay:0.0];
}];
}
Perhaps post a notification in the completion routine of the 1st animation:
UIView animateWithDuration:delay:options:animations:completion:
To anser to my own question with a nice idea:
What do you think of an executionQueue or executionBlock?
I thought, I save execution Code to a block-variable, while some animation is going on. In the animation completion block, I look, whether there is a valid executionBlock.
If there is one, execute, if not, do nothing.
Also I need to save the animation state for the block to be loaded, when a async message comes in.
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..
}