I have a situation where I have to wait for a UIKit animation to finish and I am using a completion block to execute the relevant finalising code. Now I have realised that I can trigger a race condition, which introduces errors, when I call the function twice from the main thread. I wasn't able to use a simple @synchronised(self) lock, but I have a different workaround using NSLock. I was wondering if there is a more elegant solution to this.
To give some context where this is used, I have a number of views attached to each other via UIAttachmentBehaviour (UIKit Dynamics) to have some physical animations going on. When I swipe a view, it's being replaced and the animation looks like the views are sliding in/out (simple translation). In my solution I remove the attachment behaviours or otherwise the physical attachments will follow the sliding view, which is not what I want to have.
The problematic code looks like the following:
- (void)handleGesture;
{
// [A]
// remove UIAttachmentBehaviours (UIKit dynamics) between the static views and the view that fades out
...
// animate translation of fade-out and fade-in views
[UIView animateWithDuration:0.5
animations:^{
// [B]
for (UIView* view in swipeAllViews)
{
CGPoint location = view.center;
location.x += 2*screenWidth;
view.center = location;
}
}
completion:^(BOOL finished) {
// [C]
for (UIView* view in swipeOutViews)
{
[view removeFromSuperview];
}
for (UIView* view in swipeInViews)
{
// do some setup
}
// add UIAttachmentBehaviour between the static views and the new view that fades in
}
];
}
}
Note, it's difficult to trigger the problem manually, but if you call the code snippet programmatically, the execution order is slightly different. To give an idea, I have tagged the code parts with A,B,C,D. Lets call the trace of the first execution line 1A...1D and the second call 2A...2D. Under regular circumstances the desired execution order is something like:
1A
0.5 seconds delay
1B
1C
2A
0.5 seconds delay
2B
2C
When calling handleGesture twice programmatically, the execution order, however, is:
1A
2A
0.5 seconds delay
1B
1C
2B
2C
The workaround I came up with looks like this:
- (void)viewDidLoad;
{
self.theLock = [[NSLock alloc] init];
}
- (void)handleGesture;
{
if (![self.theLock tryLock])
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^(void){
[self handleGesture];
});
}
else
{
// remove some UIAttachmentBehaviours (UIKit dynamics)
...
// animate translation of views
[UIView animateWithDuration:0.5
animations:^{
for (UIView* view in swipeAllViews)
{
CGPoint location = view.center;
location.x += 2*screenWidth;
view.center = location;
}
}
completion:^(BOOL finished) {
for (UIView* view in swipeOutViews)
{
[view removeFromSuperview];
}
for (UIView* view in swipeInViews)
{
// do some setup
}
// add UIAttachmentBehaviours between the old and new views
[self.theLock unlock];
}
];
}
}
Note, if you call lock instead of tryLock, then you will end up in a deadlock, because the code is executed on the main thread and if it blocks the animation will never finish.
In words, when handleGesture is invoked, it is locked until the animation finishes. If between or after the completion block the function is called again, it tries to obtain a lock. If it is unable to obtain it, it tries again after 0.5 seconds (approximately the time the animation should take).
Now I feel like this could become a dining philosopher's problem, which makes me wonder if there isn't a simpler solution to this. I thought putting an @synchronised(self) at the beginning of the function would solve the issue, but as the animation operation is pushed onto the main thread, the function immediately returns and the lock will be released straight away.
Thanks for reading thus far.
I would not recommend any sort of locks (either
NSLockor@synchronizedor semaphore or anything) on the main thread.You could theoretically could wrap your animations in asynchronous
NSOperationsubclass like shown here.Frankly, this all seems like a bit of a shame, given all the hard work they've put into iOS 8 for interruptible and responsive animations. See WWDC 2014 video, Building Interruptible and Responsive Interactions. The idea is that if you start a new animation half way through the prior one, have it smoothly pick up the animation from where the first one currently is, rather than waiting for it to finish or otherwise interrupting it. I'm not following the desired final UX in your case to make specific suggestions, but it might be worth watching that video and see if it generates any ideas for you.
But I'm thinking that you might be able to have state variables that indicate whether it needs to do A and whether it needs to do C. The end product would be something like:
I'm not sure if you can follow what I'm saying, but the idea might be interruptible and responsive interactions and conditional execution of the A and C blocks (i.e. only do A if this animation didn't interrupt a prior one; only do C if this animation wasn't interrupted by a subsequent one).