5

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!

Community
  • 1
  • 1
Kevin Owens
  • 538
  • 5
  • 12
  • Hi, I have implemented this code in one of my project but without success as all `ActionOperation` are not serialized: they did not wait fore previous one to start. – Dominique Vial May 09 '15 at 21:59
  • I have updated the post and code to fix a couple issues and make things clearer. This is from a working implementation, so you should be good to go. – Kevin Owens May 10 '15 at 01:53
  • So, what's the question? Don't edit the question to provide an answer — post or edit an answer. – rickster May 10 '15 at 02:20
  • I found the problem thanks to this updated code: `_finished` property was initialized to `true` on `ActionOperation` class. Thank you. – Dominique Vial May 10 '15 at 10:02
  • You're welcome. And as for the question, it's actually still there...in the title. – Kevin Owens May 11 '15 at 05:05
  • One more question: why did the operation is added on `mainQueue`? – Dominique Vial May 27 '15 at 21:14
  • That causes the operation to run on the app's main thread. – Kevin Owens May 28 '15 at 23:44
  • Ok, I understood it. My question was about the interest to make the operation run on the app's main thread. – Dominique Vial May 31 '15 at 10:36
  • I have problems using this solution: in some cases some operations ends before the complete execution of the actions. These actions are made of numerous SKActions including group, sequence and runBlock: that can be the origin of these problems. I have to achieve tests ro understand where the problem lie. So does this solution have know limitations? – Dominique Vial May 31 '15 at 17:07
  • No, I don't know of any limitations. I use the same SKActions without issue. Perhaps if you post a new question, referencing this post, that shows your implementation of this class, I can better help. And as for the use of mainQueue, this class is a Swift version of that linked at the beginning of the question. Do you have need for an alternative approach? – Kevin Owens May 31 '15 at 17:35
  • No, I have no alternatives approach. I read MainQueue is recommended for . – Dominique Vial May 31 '15 at 18:58

3 Answers3

11

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")
    }
}
rintaro
  • 51,423
  • 14
  • 131
  • 139
  • Thank you, that did it! I had tried using "is..." before but apparently had something else wrong as it wasn't working at the time. It is now, though ... thanks again. – Kevin Owens Feb 09 '15 at 06:59
1

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)

    }
}
Dominique Vial
  • 3,729
  • 2
  • 25
  • 45
  • Thanks for posting your update. If you post the code with your previous answer (instead of as a new answer) and expound upon your changes, it'll be clearer what problem you're solving and how you're solving it. It's a bit unclear to me right now. – Kevin Owens Jun 03 '15 at 13:53
  • I have added explanations on the why and the how. – Dominique Vial Jun 03 '15 at 16:02
  • 1
    Nice work. Appreciate you sharing the fruit of your labor! (One thing...I believe variable `compteur` may be a leftover from your troubleshooting.) – Kevin Owens Jun 03 '15 at 20:33
  • Oh, you're right. I have removed this useless code. – Dominique Vial Jun 03 '15 at 22:00
  • This code may have a problem! In an improved version I made to add completion block to the `runAction`, some `runAction` doesn't complete. I created a question: http://stackoverflow.com/q/30683897/540780 – Dominique Vial Jun 06 '15 at 14:20
0

There is a limitation case when SKActions transmitted during initialization is a runAction(_:onChildWithName:).

In this case the duration for this SKAction is instantaneous.

According to Apple documentation:

This action has an instantaneous duration, although the action executed on the child may have a duration of its own.

Dominique Vial
  • 3,729
  • 2
  • 25
  • 45