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];
});
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.
Say there's something I want to make sure never gets called while it's being executed already.
Take for example a UIScrollView where the scrollViewDidScroll: method is called every single time any zooming occurs. What if I wanted to use [UIView animateWithDuration...] the very first time it's called, but not allow the subsequent calls to "overwrite" the already playing animation?
dispatch_once would work perfectly, if it weren't for the fact that it only allows it to run once per program execution, when I need it to run multiple times, just not at the same time overwriting one another.
How would I go about blocking subsequent calls from messing things up?
If I understand then something like this should work (since the delegate is always called from the main thread):
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
static BOOL busy = NO;
if (!busy) {
busy = YES;
[UIView animateWithDuration:0.2 animations:^{
// your animations
} completion:^(BOOL finished) {
busy = NO;
}];
}
}
This code ensures the animation can only happen one at a time.
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.
I have 2 blocks with dispatch_sync, when the first block ends I show the window for the user and starts run the second block. But I'm not getting click any button on the screen until the second block ends..
Look the code:
[HUD showUIBlockingIndicatorWithText:#"Loading..."];
dispatch_queue_t queue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^ {
dispatch_sync(dispatch_get_main_queue(), ^{
[UIView beginAnimations:#"fade" context:nil];
[UIView setAnimationDuration:0.5];
self.capa.alpha = 0.0;
[UIView commitAnimations];
[HUD hideUIBlockingIndicator];
});
});
dispatch_barrier_async(queue, ^ {
//code executed in the background
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"entrou na foto");
//[self getFotos];
});
});
If you call any code on the main thread, it will block your UI, so calling
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(#"entrou na foto");
[self getFotos];
});
will block your UI until [self getFotos] has returned.
A couple of reactions.
The pattern you've used here doesn't quite make sense. Making some inferences from your code sample, I would have thought that the desired pattern would be:
Just start the HUD (or some spinning activity indicator view) in the main queue;
Dispatch all of your separate time consuming processes in the background queue; if they can operate concurrently, you'd use dispatch_async to your queue; and
Do a final dispatch_barrier_async of your completion block (i.e. your indication that all of the other blocks dispatched to your concurrent queue are done), which does the dispatch_async (not sync, generally) back to the main queue to stop the HUD.
There's nothing in this code sample that would suggest anything that would make your UI unresponsive, so I would have to suspect something in the portions of code that you've removed for the sake of clarity/brevity. In general, there are two things that might make your UI unresponsive:
Obviously, if there's anything dispatched back to the main queue that is slow. For example, you have a getFotos method, but we don't know what that does, but if it was slow, that would cause a problem. There's nothing obviously in this category of your snippet, but it's one class of problem to be aware of.
The more subtle problem can be if there's something in that slipped into that background queue accidentally that is UI related. Ironically, that can often cause a UI to freeze for a bit. You'd have to share the details of what you're doing in that block dispatched to the background for us to advise you further on that.
But those are the only two things that leap out at me that could cause your UI to become temporarily unresponsive.
Completely unrelated to your performance issues, but I wouldn't generally advise the old-style animations. As the docs say, "... this method is discouraged in iOS 4.0 and later." So I'd suggest removing the lines that say:
[UIView beginAnimations:#"fade" context:nil];
[UIView setAnimationDuration:0.5];
self.capa.alpha = 0.0;
[UIView commitAnimations];
Instead, I'd suggest you to use block-based animation, such as:
[UIView animateWithDuration:0.5 animations:^{
self.capa.alpha = 0.0;
}];
You may want to use dispatch groups here. Dispach a block into background & into the group with the UI disabled, and then you can spawn a waiting thread calling dispatch_group_wait blocking it until task is done to re-enable the UI. Only the disabled parts are locked. and everything else will work
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, /* queue */, ^{ /* ... */ });
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0),
^{
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// This thread will be blocked until background tasks are done.
dispatch_async(dispatch_get_main_queue(),
^{ /* ... */ });
});