Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to subclass NSOperation in Swift to queue SKAction objects for serial execution?

Rob provided a great Objective-C solution for subclassing NSOperation to achieve a serial queuing mechanism for SKAction objects. I implemented this successfully in my own Swift project.

import SpriteKit

class ActionOperation : NSOperation
{
    let _node: SKNode // The sprite node on which an action is to be performed
    let _action: SKAction // The action to perform on the sprite node
    var _finished = false // Our read-write mirror of the super's read-only finished property
    var _executing = false // Our read-write mirror of the super's read-only executing property

    /// Override read-only superclass property as read-write.
    override var executing: Bool {
        get { return _executing }
        set {
            willChangeValueForKey("isExecuting")
            _executing = newValue
            didChangeValueForKey("isExecuting")
        }
    }

    /// Override read-only superclass property as read-write.
    override var finished: Bool {
        get { return _finished }
        set {
            willChangeValueForKey("isFinished")
            _finished = newValue
            didChangeValueForKey("isFinished")
        }
    }

    /// Save off node and associated action for when it's time to run the action via start().
    init(node: SKNode, action: SKAction) {

    // This is equiv to ObjC:
    // - (instancetype)initWithNode(SKNode *)node (SKAction *)action
    // See "Exposing Swift Interfaces in Objective-C" at https://developer.apple.com/library/mac/documentation/Swift/Conceptual/BuildingCocoaApps/InteractingWithObjective-CAPIs.html#//apple_ref/doc/uid/TP40014216-CH4-XID_35

        _node = node
        _action = action
        super.init()
    }

    /// Add the node action to the main operation queue.
    override func start()
    {
        if cancelled {
            finished = true
            return
        }

        executing = true

        NSOperationQueue.mainQueue().addOperationWithBlock {
            self._node.runAction(self._action) {
                self.executing = false
                self.finished = true
            }
        }
    }
}

To use the ActionOperation, instantiate an NSOperationQueue class member in your client class:

var operationQueue = NSOperationQueue()

Add this important line in your init method:

operationQueue.maxConcurrentOperationCount = 1; // disallow follow actions from overlapping one another

And then when you are ready to add SKActions to it such that they run serially:

operationQueue.addOperation(ActionOperation(node: mySKNode, action: mySKAction))

Should you need to terminate the actions at any point:

operationQueue.cancelAllOperations() // this renders the queue unusable; you will need to recreate it if needing to queue anymore actions

Hope that helps!

like image 845
Kevin Owens Avatar asked Feb 06 '15 19:02

Kevin Owens


2 Answers

According to the document:

In your custom implementation, you must generate KVO notifications for the isExecuting key path whenever the execution state of your operation object changes.

In your custom implementation, you must generate KVO notifications for the isFinished key path whenever the finished state of your operation object changes.

So I think you have to:

override var executing:Bool {
    get { return _executing }
    set {
        willChangeValueForKey("isExecuting")
        _executing = newValue
        didChangeValueForKey("isExecuting")
    }
}

override var finished:Bool {
    get { return _finished }
    set {
        willChangeValueForKey("isFinished")
        _finished = newValue
        didChangeValueForKey("isFinished")
    }
}
like image 91
rintaro Avatar answered Jan 03 '23 21:01

rintaro


I want to group animations for several nodes. I first tried the solution above by grouping all actions in one, using runAction(_:onChildWithName:) to specify which actions have to been done by node.

Unfortunately there were synchronisation problems because in the case of runAction(_:onChildWithName:) the duration for SKAction is instantaneous. So I have to found another way to group animations for several nodes in one operation.

I then modified the code above by adding an array of tuples (SKNode,SKActions).

The modified code presented here add the feature to init the operation for several nodes, each one of them having it's own actions.

For each node action is run inside it's own block added to the operation using addExecutionBlock. When an action complete, a completion block is executed calling checkCompletion() in order to join them all. When all actions have completed then the operation is marked as finished.

class ActionOperation : NSOperation
{

    let _theActions:[(SKNode,SKAction)]
    // The list of tuples :
    // - SKNode     The sprite node on which an action is to be performed
    // - SKAction   The action to perform on the sprite node

    var _finished = false // Our read-write mirror of the super's read-only finished property
    var _executing = false // Our read-write mirror of the super's read-only executing property

    var _numberOfOperationsFinished = 0 // The number of finished operations


    override var executing:Bool {
        get { return _executing }
        set {
            willChangeValueForKey("isExecuting")
            _executing = newValue
            didChangeValueForKey("isExecuting")
        }
    }

    override var finished:Bool {
        get { return _finished }
        set {
            willChangeValueForKey("isFinished")
            _finished = newValue
            didChangeValueForKey("isFinished")
        }
    }


    // Initialisation with one action for one node
    //
    // For backwards compatibility
    //
    init(node:SKNode, action:SKAction) {
        _theActions = [(node,action)]
        super.init()
    }

    init (theActions:[(SKNode,SKAction)]) {
        _theActions = theActions
        super.init()
    }

    func checkCompletion() {
        _numberOfOperationsFinished++

        if _numberOfOperationsFinished ==  _theActions.count {
            self.executing = false
            self.finished = true
        }

    }

    override func start()
    {
        if cancelled {
            finished = true
            return
        }

        executing = true

        _numberOfOperationsFinished = 0
        var operation = NSBlockOperation()

        for (node,action) in _theActions {

            operation.addExecutionBlock({
                node.runAction(action,completion:{ self.checkCompletion() })
            })
        }

        NSOperationQueue.mainQueue().addOperation(operation)

    }
}
like image 45
Dominique Vial Avatar answered Jan 03 '23 22:01

Dominique Vial