Search code examples
swiftsprite-kitclosuresskactioncompletion

Completion block never called at end of SKAction sequence of groups


Why is the completion never called?

I'm terribly sorry about this, a code dump... because I have no idea why every part of this works, except for the calling of the completion.

The SKAction that's not calling its completion runs, all except for the completion. It is this:

curtain.run(fadeMoveWipeAndReveal, completion: {onDone(), print("I'm done!!!!")})

Within the following class:

import SpriteKit

class WipeCurtain: SKSpriteNode {

    var wipeCurtainBase: SKSpriteNode?
    var returnable = SKNode()
    var moveRight = SKAction()
    var node = SKNode()

    init(     color: SKColor,
              size: CGSize,
              time: TimeInterval,
              reveal: @escaping () -> (),
              onDone: @escaping () -> ())
    {
        super.init(texture: nil, color: color, size: size)
        wipeCurtainBase = SKSpriteNode(color: color, size: size)

        let show = SKAction.run(reveal)

        let fadeIn = createFadeIn(duration: time)
        let moveRight = createMoveRight(duration: time)
        let wipeRight = createWipeRight(duration: time)

        let fadeAndMove = SKAction.group( [ fadeIn, moveRight ] )
        let wipeAndReveal = SKAction.group( [ show, wipeRight ] )
        let fadeMoveWipeAndReveal = SKAction.sequence( [ fadeAndMove, wipeAndReveal ] )

        if let  curtain = self.wipeCurtainBase {
            curtain.anchorPoint = CGPoint(x: 1, y: 0)
            curtain.position = CGPoint(x: frame.size.width, y: 0)
            curtain.zPosition = -1
            curtain.name = "wipe"
            curtain.run(fadeMoveWipeAndReveal, completion: {
                onDone()
                print("I'm done!!!!")
            })
        }
    }

    func createFadeIn(duration: TimeInterval) -> SKAction {
        let fadeIn = SKEase
            .fade(
                easeFunction: .curveTypeLinear,
                easeType: .easeTypeIn,
                time: duration,
                fromValue: 0,
                toValue: 1
            )
        return fadeIn
        }

    func createMoveRight(duration: TimeInterval) -> SKAction {
        let moveRight = SKEase
            .move(
                easeFunction: .curveTypeExpo,
                easeType: .easeTypeOut,
                duration: duration,
                origin: CGPoint(
                    x: 0,
                    y: 0),
                destin: CGPoint(
                    x: frame.size.width,
                    y: 0)
            )
        return moveRight
        }

    func createWipeRight(duration: TimeInterval) -> SKAction {
        let wipeRight = SKEase
            .createFloatTween(
                start: 1,
                ender: 0,
                timer: duration,
                easer: SKEase
                    .getEaseFunction(
                        .curveTypeExpo,
                        easeType: .easeTypeOut
                    ),
            setterBlock: {(node, i) in
                node.xScale = i}
            )
        return wipeRight
        }

    func wipeWith() -> SKNode {
        if let curtain = wipeCurtainBase?.copy() as! SKSpriteNode? {
            returnable = curtain
            }
        return returnable
        }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

Update:

Here's the gameScene that makes this work, a very lightly modded version of the Xcode SpriteKit template:

import SpriteKit
import GameplayKit

class GameScene: SKScene {

    private var swipe: WipeCurtain?

    override func didMove(to view: SKView) {
        swipe = WipeCurtain(color: .brown, size: frame.size, time: 1,
            reveal: self.showOrNot,
            onDone: self.previewer
            )
        }

    func showOrNot(){
        print(" Deciding to show or not.......")
        }

    func previewer(){
        print("now running the previewer")
        }

    func touchDown(atPoint pos : CGPoint) {
        addChild(swipe!.wipeWith())
        }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        for t in touches { self.touchDown(atPoint: t.location(in: self)) }
        }
}

Solution

  • I want to focus your attention to these few lines:

    enter image description here

    The offending code is:

    curtain.run(fadeMoveWipeAndReveal, completion: {
                    onDone()
                    print("I'm done!!!!")
    })
    

    where you can substitute fadeMoveWipeAndReveal with one of the other action like simply show:

    let show = SKAction.run(reveal)
    

    and you will see the completion is never called. Why? Because it run the first time during the WipeCurtain initialization but the action is removed before it completes, so the next time you re-call this one to run through touchesBegan, completion will never called.

    You can test it by putting a breakpoint to the curtain.run code and launch the project:

    enter image description here

    as you can see the breakpoint stop the project immediatly during initialization, in this phase, action is removed before it completes.

    About your workaround,that's correct, it works because you remove completion and use the simply SKAction.sequence that is executed correctly every time you call it.


    Details: The copy method works for me, I suspect some people have problems with it because around internet there are more versions about the SKEase and maybe some of that could have a problem, but this version works well as demonstrated by these two screenshots below:

    enter image description here

    enter image description here