I dealt with this problem by playing a game with unfolding tiles. I wanted to both prevent keyboard input and wait as short as possible to perform another action, while the tiles really moved.
All the tiles that I was worried about were instances of the same SKNode subclass, so I decided to give this class the ability to keep track of animations in the process and respond to requests about whether animations were running.
The idea I had was to use a dispatch group to "count" activity: it has a built-in mechanism to wait on, and it can be added at any time, so the wait will continue until the tasks are added to the group. *
This is a sketch of the solution. We have a node class that creates and owns a dispatch group. A private class method allows instances to access a group so that they can enter and leave it when animating. The class has two public methods that allow you to check the status of a group without exposing the actual mechanism: +waitOnAllNodeMovement and +anyNodeMovementInProgress . The former block until the group is empty; the latter simply returns BOOL immediately, indicating whether the group is busy or not.
@interface WSSNode : SKSpriteNode + (void)waitOnAllNodeMovement; + (BOOL)anyNodeMovementInProgress; - (void)moveToPosition:(CGPoint)destination; @end
@interface WSSNode () + (dispatch_group_t)movementDispatchGroup; @end @implementation WSSNode + (void)waitOnAllNodeMovement { dispatch_group_wait([self movementDispatchGroup], DISPATCH_TIME_FOREVER); } + (BOOL)anyNodeMovementInProgress { // Return immediately regardless of state of group, but indicate // whether group is empty or timeout occurred. return (0 != dispatch_group_wait([self movementDispatchGroup], DISPATCH_TIME_NOW)); } + (dispatch_group_t)movementDispatchGroup { static dispatch_group_t group; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ group = dispatch_group_create(); }); return group; } - (void)moveToPosition:(CGPoint)destination { // No need to actually enqueue anything; simply manually // tell the group that it working. dispatch_group_enter([WSSNode movementDispatchGroup]); [self runAction:/* whatever */ completion:^{ dispatch_group_leave([WSSNode movementDispatchGroup])}]; } @end
A controller class that wants to prevent keyboard input during moves can do something simple:
- (void)keyDown:(NSEvent *)theEvent {
and you can do the same in the update: scene update: as needed. Any other actions that should happen as soon as possible can wait for the animation:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [WSSNode waitOnAllNodeMovement]; dispatch_async(dispatch_get_main_queue(), ^{
This is one hard / dirty part of this solution: since the wait... method is blocked, it obviously should happen asynchronously with the main thread; then we return to the main topic to do more work. But the same would be true with any other waiting procedure, so this seems reasonable.
* Two other possibilities that seemed to represent a queue with a barrier block and a counting semaphore.
The barrier unit will not work, because I did not know when I could put it in the queue. At that moment, when I decided to queue the task βafterβ, no tasks βbeforeβ could be added.
The semaphore will not work, because it does not control the order, just simultaneity. If the nodes increased the semaphore when they were created, decreased during the animation, and increased again when it was done, another task could only wait if all the created nodes were animated, and would not wait longer than the first completion. If the nodes at first did not increase the semaphore, then only one of them could function at a time.
The send group is used as a semaphore, but with privileged access: the nodes themselves should not wait.