Completion block never called at end of SKAction sequence of groups
Asked Answered
C

3

7

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)) }
        }
}
Cartouche answered 5/12, 2016 at 21:6 Comment(7)
But the animation takes place like expected?Den
Yes, everything else sequences perfectly, @DenCartouche
@Den added the gameScene code. This is all based off the SpriteKit template file in Xcode. If you copy paste these two files into that template, it'll run.Cartouche
you have a peculiar way of writing code.Eiser
@JozemiteApps that might be a grotesque understatement.Cartouche
@JozemiteApps i'm not a programmer, I'm a designer with some ancient ideas on how text editors should be removed from coding. But I greatly appreciate you taking the time to peruse my code, and make the comment. There are some advantages to this layout for my kind of editing... but I don't think anyone else would find it agreeable ;)Cartouche
@JozemiteApps everything about Confused's code is a grotesque understatement :PCarpophore
V
3

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

Veta answered 17/12, 2016 at 10:4 Comment(2)
how does this affect basic functionality of .copy() not copying the completion block details?Carpophore
Hello Fluidity, the copy method works. I've replicated the OP code and I've also checked the copy code with two breakpoints ,where I've maked also two screenshots, it copied correctly the wipeCurtainBase sprite. You can see that results to the bottom part of this answer. I don't know why your code don't work,maybe probably due to SKEase library version.Veta
C
4

Using a run(block: () -> ()) action in the final sequence gets the desired result, but still doesn't help me understand why the completion is never getting called. This is just a workaround:

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 endBlock = SKAction.run( onDone )
        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, endBlock ]
            )

        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)
        }
    }

    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")
    }
}
Cartouche answered 5/12, 2016 at 21:36 Comment(0)
V
3

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

Veta answered 17/12, 2016 at 10:4 Comment(2)
how does this affect basic functionality of .copy() not copying the completion block details?Carpophore
Hello Fluidity, the copy method works. I've replicated the OP code and I've also checked the copy code with two breakpoints ,where I've maked also two screenshots, it copied correctly the wipeCurtainBase sprite. You can see that results to the bottom part of this answer. I don't know why your code don't work,maybe probably due to SKEase library version.Veta
C
1

.copy() doesn't copy over blocks or closures in this way. It's either a bug, lack of planning by Apple, or some principle that they didn't disclose to anyone. It may be NSObject thing, because you can't persist blocks either, or use NSCoder with them (afaik).

From a pre-Xcode 8 perspective, none of this should do anything at all AFAIK, except CRASH.. You only call the initializer once (the thing that does the actual work with curtain), and you didn't initially add it to the scene.

I would suggest taking a different approach with this, or using a workaround as you came up with. Perhaps initializing a new node is faster even than .copy(). Testing would need to be done to see how performant it is.

Bounty update:

my guess is NSObject and NSCoder conformance of SKNode… all nodes conform to NSCoder and you can’t use NSCoder with closures / blocks, at least not anyway that I'm aware of.. even trying to convert to/from NSData or NSString.

Carpophore answered 14/12, 2016 at 14:35 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.