Working on a little Sprite Kit game, I had the problem that I wanted a couple of things to wait until a group of sprite nodes had stopped their animation.
I decided to use a GCD dispatch group to monitor the sprites' actions. The nodes that I was concerned about were all instances of the same class, so I gave that class a private method vending a dispatch_group_t
that the instances can enter and leave as they started and ended their animations.
The class has two public methods that allow checking the status of the animations: one that blocks until the dispatch group is empty, and one that simply returns the group's busy status.
@interface WSSNode : SKSpriteNode
/** The WSSNode class tracks whether any instances are running animations,
* in order to avoid overlapping other actions.
* +waitOnAllNodeMovement blocks when called until all nodes have
* completed their animations.
*/
+ (void)waitOnAllNodeMovement;
/** The WSSNode class tracks whether any instances are running animations,
* in order to avoid overlapping other actions.
* +anyNodeMovementInProgress returns a BOOL immediately, indicating
* whether any animations are currently running.
*/
+ (BOOL)anyNodeMovementInProgress;
/* Sample method: make the node do something that requires waiting on. */
- (void)moveToPosition:(CGPoint)destination;
@end
@interface WSSNode ()
/** Exposes the private dispatch group that the class uses to track and
* report on running animations.
*/
+ (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 increment/decrement
// group counter manually.
dispatch_group_enter([WSSNode movementDispatchGroup]);
[self runAction:/* whatever */
completion:^{ dispatch_group_leave([WSSNode movementDispatchGroup])}];
}
@end
An example of the intended usage of +anyNodeMovementInProgress
would be:
- (void)keyDown:(NSEvent *)theEvent
{
// Don't accept input while movement is taking place.
if( [WSSNode anyNodeMovementInProgress] ){
return;
}
// ...
}
The three things that I'm specifically interested to have reviewed are:
- I think that the simplicity of the solution makes concurrency problems unlikely, but I'm far from an expert on the subject. Is there any potential concurrency problem here?
- Is the interface for waiting/checking status clear and usable? Do the methods do what you expect given their names?
- Sub-point: are the doc comments clear and thorough enough?
The way that the "wait" functionality is exposed makes users of the class have to do a little dispatch queue hop to avoid blocking the main thread:
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ [WSSNode waitOnAllNodeMovement]; dispatch_async(dispatch_get_main_queue(), ^{ // Action that needs to wait for animation to finish }); });
Would this be more palatable if the
WSSNode
class accepted a Block to be run after the group is empty? I didn't like that idea because it seemed like a muddying of responsibility, but I'm interested in contrary thoughts.
I posted this solution on Stack Overflow, including my reasoning for not using a seemingly-more-straightforward mechanism such as a dispatch semaphore or queue with a barrier Block.
If the nodes incremented the semaphore when they were created, decremented when animating, and incremented again when done, the other task would only wait if all created nodes were animating, and wouldn't wait any longer than the first completion. If the nodes didn't increment the semaphore initially, then only one of them could function at a time.
The barrier Block wouldn't work because I didn't know when I could actually enqueue it. At the point where I decided to enqueue the "after" task, no "before" tasks could be added.
Suggestions on that: that is, alternate mechanisms and/or flaws in my reasoning are also welcome.