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)) }
        }
}
Confused
  • 6,048
  • 6
  • 34
  • 75
  • But the animation takes place like expected? – shallowThought Dec 05 '16 at 21:14
  • Yes, everything else sequences perfectly, @shallowThought – Confused Dec 05 '16 at 21:19
  • @shallowThought 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. – Confused Dec 05 '16 at 21:29
  • 1
    you have a peculiar way of writing code. – Hedylove Dec 06 '16 at 08:00
  • @JozemiteApps that might be a grotesque understatement. – Confused Dec 06 '16 at 08:04
  • @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 ;) – Confused Dec 06 '16 at 08:08
  • 1
    @JozemiteApps everything about Confused's code is a grotesque understatement :P – Fluidity Dec 14 '16 at 14:10

3 Answers3

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")
    }
}
Confused
  • 6,048
  • 6
  • 34
  • 75
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

Alessandro Ornano
  • 34,887
  • 11
  • 106
  • 133
  • how does this affect basic functionality of `.copy()` not copying the completion block details? – Fluidity Dec 27 '16 at 05:54
  • 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. – Alessandro Ornano Dec 27 '16 at 09:13
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.

Community
  • 1
  • 1
Fluidity
  • 3,985
  • 1
  • 13
  • 34