Search code examples
swiftmemory-managementautomatic-ref-countingexc-bad-accessnsoperationqueue

Swift <uninitialized> let property


I need some debugging help, because an error I run into is really hard.

This is a game with complex animations. However the question isn't about SpriteKit. I want animations to follow each other in strict order, so I implemented a subclass of Operation:

class ActionOperation: Operation
{
    var debugLabel: String?

    private(set) var actionNodes: Set<ActionNode>

    //... Other vars, isFinished etc.

    init(node: SKNode, action: SKAction) {
       actionNodes = [ActionNode(node: node, action: action)]
       super.init()
    }

    init(nodesAndActions: [(SKNode?, SKAction)]) {
        actionNodes = Set(nodesAndActions.map( {(tuple) in return 
            ActionNode(node: tuple.0, action: tuple.1)
        }))
        super.init()
    }

    override func start() { /* ... */ }
}

For this class details you can see the source question. A helper struct:

extension ActionOperation {
    struct ActionNode: Hashable {
        static func ==(lhs: ActionOperation.ActionNode, rhs: ActionOperation.ActionNode) -> Bool {
            return lhs.node == rhs.node && lhs.action == rhs.action
        }

        weak var node: SKNode?

        // This constant is causing problems!
        let action: SKAction

        let setIdForHash: Int

        var hashValue: Int { return setIdForHash ^ action.hashValue }
    }
}

The Problem

The instances of the ActionOperation are added to the animationQueue. Queue setup:

fileprivate let animationQueue = OperationQueue()

// Setup:
animationQueue.maxConcurrentOperationCount = 1
animationQueue.qualityOfService = .userInteractive

Operation setup:

let duration = 0.35
var groupActions = [SKAction]()
for n in 0..<from.count {
    let fromIndex = from[n]
    let toIndex = to[n]
    let move = SKAction.move(to: fieldPosition(at: toIndex), duration: duration)
    let seq = SKAction.sequence([
        SKAction.run(move, onChildWithName: "\(fromIndex)"),
        SKAction.wait(forDuration: duration),
        SKAction.run({
            self.piece(at: fromIndex)?.name = "\(toIndex)"
        })
    ])
    groupActions.append(seq)
}
let gravOperation = ActionOperation(node: piecesLayer, action: SKAction.group(groupActions))
gravOperation.debugLabel = "gravity"
animationQueue.addOperation(gravOperation)

Sometimes this queue get stack, that means one operation is executing forever. I'm trying to debug it using Xcode command line and type this:

p (animationQueue.operations[0] as! ActionOperation).actionNodes.first!.action

(SKAction) $R6 = <uninitialized>

What does it mean? How a let constant in a struct can be uninitialized?

If I print node, everything is ok:

po (animationQueue.operations[0] as! ActionOperation).actionNodes.first!.node

<SKNode> name:'(null)' position:{0, 0} scale:{1.00, 1.00} accumulatedFrame:{{178.13499450683594, -26.469999313354492}, {908.33502197265625, 1012.9400024414062}}


Solution

  • Well, the problem was caused by the way I crate group action:

    let gravOperation = ActionOperation(node: piecesLayer, action: SKAction.group(groupActions))
    

    When groupActions array is empty action will never be executed on the node and if we call:

    node.run(badGroupAction, completion: { /* never called */ })
    

    the completion will be never called. That's why queue was stuck. I think it's a SpriteKit flaw. I would prefer completion to be called immediately if we try to run empty group or sequence.

    Why does action become uninitialized? I don't know, I think it's a bug, probably related to the empty array.