At the bottom, in my original answer, I describe a way to achieve the requested functionality (if you initiate an animation while the prior animation is still in progress, queue up this subsequent animation to only start after the current ones are done).
While I'll keep that for historical purposes, I might want to suggest an entirely different approach. Specifically, if you tap on a button that should result in an animation, but a previous animation is still in progress, I would instead suggest that you remove the old animation and immediately start the new animation, but do so in manner such that the new animation picks up from wherever the current one left off.
In iOS versions prior to iOS 8, the challenge is that if you start a new animation while another is in progress, the OS awkwardly immediately jumps to where the current animation would have ended and starts the new animation from there.
The typical solution in iOS versions prior to 8 would be to:
grab the
presentationLayer
of the animated view (this is the current state of theCALayer
of theUIView
... if you look at theUIView
while the animation is in progress, you'll see the final value, and we need to grab the current state);grab the current value of the animated property value from that
presentationLayer
;remove the animations;
reset the animated property to the "current" value (so that it doesn't appear to jump to the end of the prior animation before starting the next animation);
initiate the animation to the "new" value;
So, for example, if you are animating the changing of a
frame
which might be in progress of an animation, you might do something like:CALayer *presentationLayer = animatedView.layer.presentationLayer; CGRect currentFrame = presentationLayer.frame; [animatedView.layer removeAllAnimations]; animatedView.frame = currentFrame; [UIView animateWithDuration:1.0 animations:^{ animatedView.frame = newFrame; }];
This completely eliminates all of the awkwardness associated with queuing up the "next" animation to run after the "current" animation (and other queued animations) completes. You end up with a far more responsive UI, too (e.g. you don't have to wait for prior animations to finish before the user's desired animation commences).
In iOS 8, this process is infinitely easier, where it will, if you initiate a new animation, it will often start the animation from not only the current value of the animated property, but will also identify the speed at which that this currently animated property is changing, resulting in a seamless transition between the old animation and the new animation.
For more information about this new iOS 8 feature, I'd suggest you refer to WWDC 2014 video Building Interruptible and Responsive Interactions.
For the sake of completeness, I'll keep my original answer below, as it tries to precisely tackle the functionality outlined in the question (just uses a different mechanism to ensure the main queue isn't blocked). But I'd really suggest considering stopping the current animation and starting the new one in such a manner that it commences from wherever any in-progress animation might have left off.
Original answer:
I wouldn't recommend wrapping an animation in a NSLock
(or semaphore, or any other similar mechanism) because that can result in blocking the main thread. You never want to block the main thread. I think your intuition about using a serial queue for resizing operations is promising. And you probably want a "resizing" operation that:
initiates the
UIView
animation on the main queue (all UI updates must take place on the main queue); andin the animation's completion block, finish the operation (we don't finish the operation until then, to ensure that other queued operations don't initiate until this one finished).
I might suggest a resizing operation:
SizeOperation.h:
@interface SizeOperation : NSOperation
@property (nonatomic) CGFloat sizeChange;
@property (nonatomic, weak) UIView *view;
- (id)initWithSizeChange:(NSInteger)change view:(UIView *)view;
@end
SizingOperation.m:
#import "SizeOperation.h"
@interface SizeOperation ()
@property (nonatomic, readwrite, getter = isFinished) BOOL finished;
@property (nonatomic, readwrite, getter = isExecuting) BOOL executing;
@end
@implementation SizeOperation
@synthesize finished = _finished;
@synthesize executing = _executing;
- (id)initWithSizeChange:(NSInteger)change view:(UIView *)view
{
self = [super init];
if (self) {
_sizeChange = change;
_view = view;
}
return self;
}
- (void)start
{
if ([self isCancelled] || self.view == nil) {
self.finished = YES;
return;
}
self.executing = YES;
// note, UI updates *must* take place on the main queue, but in the completion
// block, we'll terminate this particular operation
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[UIView animateWithDuration:2.0 delay:0.0 options:kNilOptions animations:^{
CGRect frame = self.view.frame;
frame.size.width += self.sizeChange;
self.view.frame = frame;
} completion:^(BOOL finished) {
self.finished = YES;
self.executing = NO;
}];
}];
}
#pragma mark - NSOperation methods
- (void)setExecuting:(BOOL)executing
{
[self willChangeValueForKey:@"isExecuting"];
_executing = executing;
[self didChangeValueForKey:@"isExecuting"];
}
- (void)setFinished:(BOOL)finished
{
[self willChangeValueForKey:@"isFinished"];
_finished = finished;
[self didChangeValueForKey:@"isFinished"];
}
@end
Then define a queue for these operations:
@property (nonatomic, strong) NSOperationQueue *sizeQueue;
Make sure to instantiate this queue (as a serial queue):
self.sizeQueue = [[NSOperationQueue alloc] init];
self.sizeQueue.maxConcurrentOperationCount = 1;
And then, anything that makes the view in question grow, would do:
[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:+50.0 view:self.barView]];
And anything that makes the view in question shrink, would do:
[self.sizeQueue addOperation:[[SizeOperation alloc] initWithSizeChange:-50.0 view:self.barView]];
Hopefully this illustrates the idea. There are all sorts of possible refinements:
I made the animation really slow, so I could easily queue up a whole bunch, but you'd probably be using a much shorter value;
If using auto layout, you'd be adjusting the width constraint's
constant
and in the animation block you'd perform alayoutIfNeeded
) rather than adjusting the frame directly; andYou probably want to add checks to not perform the frame change if the width has hit some maximum/minimum values.
But the key is that using locks to control animation of UI changes is inadvisable. You don't want anything that could block the main queue for anything but a few milliseconds. Animation blocks are too long to contemplate blocking the main queue. So use a serial operation queue (and if you have multiple threads that need to initiate changes, they'd all just add an operation to the same shared operation queue, thereby automatically coordinating changes initiated from all sorts of different threads).