Search code examples
swiftsprite-kitinitializationsubclassskspritenode

Circling the drain of Initialization in subclass: Swift and SpriteKit


I want to create a SKShapeNode at a higher level than the touchesBegan of a SpriteNode so when I want to add the SKShapeNode to the screen from the touchesBegun event on this sprite, the shape already exists, and I simply add it to the screen from within the touchesBegan override.

TL;DR, in my SKSpriteNode, I'm trying to pre-build the SKShapeNode that will be used as an animated ring when the Sprite is touched.

I'd like to create the SKShapeNode with variable/constants, so I can easily edit its values...

So in the root of the subclass I have variables for color, size, and linewidth, ready to be used in the creation of the SKShapeNode...

class Balls: SKSpriteNode {

    var ringSize: CGFloat = 64
    var ringColor: SKColor = SKColor.white
    var ringWidth: CGFloat = 16

....

Further down, but still at the root of the class, I create my ring:

 let ring = SKShapeNode(circleOfRadius: ringSize)

And am instantly greeted with the lovingly cryptic:

Can not use instance member 'ringSize' within property initializer, property initializers run before 'self' is available.

Fine. Ok. I get it. You want to think that a functional call to a class to create a property should be done before values are assigned to self. Neither here nor there, I think I'm getting cunning and can get around that by wrapping everything in a function:

class Balls: SKSpriteNode {

        var ringSize: CGFloat = 64
        var ringColor: SKColor = SKColor.white
        var ringWidth: CGFloat = 16

        var myRing = SKShapeNode()    

    func createRing() -> SKShapeNode{
            let ring = SKShapeNode(circleOfRadius: ringSize)
                ring.strokeColor = ringColor
                ring.lineWidth = ringWidth
            return ring
        }

This generates no errors, and my excitement builds.

So I add another line, to create the actual ring: ....

 myRing = createRing()

Dead again:

! Expected declaration

I have absolutely no idea what this means and began to randomly attempt weird things.

One of them is heading into my already messy convenience initializer and adding myRing = createRing() in there... and this WORKS!

How and why does this work, and is this the best/right/proper way to be circling the drain of initialization?

:: EDIT:: UPDATE :: Full Code Context ::

Here's the full class with my bizarre and misunderstood initialisers.

import SpriteKit

class Circle: SKSpriteNode {

    var ringSize: CGFloat = 96
    var ringColor: SKColor = SKColor.white
    var ringWidth: CGFloat = 8

    var myRing = SKShapeNode()


    override init(texture: SKTexture?, color: UIColor, size: CGSize) {
        super.init(texture: texture, color: color, size: size)
    }

    convenience init() {
        self.init(color: SKColor.clear, size: CGSize(width: 100, height: 100))
        myRing = createRing()
        addChild(myRing)
        print("I'm on the screen")
        explodeGroup = create_explosionActionGroup()
        }

    convenience init(color: UIColor, size: CGSize, position: CGPoint) {
        self.init(color: color, size: size)
        self.position = position

        myRing = createRing()
        explodeGroup = create_explosionActionGroup()

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

    func createRing() -> SKShapeNode{
        let ring = SKShapeNode(circleOfRadius: ringSize)
        ring.strokeColor = ringColor
        ring.lineWidth = ringWidth
        return ring
    }

Solution

  • And am instantly greeted with the lovingly cryptic:

    Can not use instance member 'ringSize' within property initializer, property initializers run before 'self' is available.

    So one way around this problem would be to make the default ringSize available another way, e.g.

    static let defaultRingSize: CGFloat = 64
    
    var ringSize: CGFloat = Circle.defaultRingSize
    let ring = SKShapeNode(circleOfRadius: Circle.defaultRingSize)
    

    ... but I question why you even have a var ringSize property like that. Shouldn't you have a didSet observer on it, so that if you change its value, you can update the shape of ring?

    Dead again:

    ! Expected declaration

    You weren't clear, in your question, how you actually triggered this, but I guess you tried something like this:

    class Circle: SKSpriteNode {
    
        var ringSize: CGFloat = 96
    
        var myRing = SKShapeNode()
        myRing = createRing() // “Expected declaration” error on this line
    

    The problem here is that you've placed a statement in the body of your class, but only declarations are allowed in the body.

    One of them is heading into my already messy convenience initializer and adding myRing = createRing() in there... and this WORKS!

    How and why does this work

    All of your class's own instance variables must be initialized before a super.init call. Since myRing has a default value, the compiler effectively inserts the initialization of myRing before the call to super.init in your designated initializer, like this:

    override init(texture: SKTexture?, color: UIColor, size: CGSize) {
        // Compiler-inserted initialization of myRing to the default
        // value you specified:
        myRing = SKShapeNode()
    
        super.init(texture: texture, color: color, size: size)
    }
    

    Since you declared var myRing, you can then change it later to the customized SKShapeNode you really want.

    is this the best/right/proper way to be circling the drain of initialization?

    Well, “circling the drain” means “failing”, so I guess you're asking if this is “the best/right/proper way” to fail at initialization… I suppose it's not the best way to fail, since you didn't actually fail in the end.

    Or maybe you meant “I hate the way Swift does initialization so I'm going to throw some shade”, in which case, you ain't seen nothin' yet.

    But maybe you really meant “is this the best/right/proper way to initialize my instance”, in which case, well, “best” and “right” and “proper” are pretty subjective.

    But I can objectively point out that you're creating an SKShapeNode (as the default value of myRing) just to immediately throw it away and create another SKShapeNode. So that's a waste. You've also got calls to createRing in both of your convenience initializers, but you could factor them out into the designated initializer.

    But I wouldn't even do it quite like that. SKShapeNode's path property is settable, so you can just create a default SKShapeNode and then change its path after the call to super.init. That also makes it easier to handle changes to ringSize and the other properties, because you can funnel all the changes through a single method that knows how to make myRing match the properties.

    Here's how I'd probably write your class:

    import SpriteKit
    
    class Circle: SKSpriteNode {
    
        var ringSize: CGFloat = 96 {
            // Use an observer to update myRing if this changes.
            didSet { configureMyRing() }
        }
    
        var ringColor = SKColor.white {
            didSet { configureMyRing() }
        }
    
        var ringWidth: CGFloat = 8 {
            didSet { configureMyRing() }
        }
    
        // This can be a let instead of a var because I'm never going to
        // set it to a different object. Note that I'm not bothering to
        // initialize myRing's path or any other property here, because
        // I can just call configureMyRing in my init (after the call to
        // super.init).
        let myRing = SKShapeNode()
    
        override init(texture: SKTexture?, color: SKColor, size: CGSize) {
            super.init(texture: texture, color: color, size: size)
    
            // Call this now to set up myRing's path and other properties.
            configureMyRing()
        }
    
        convenience init() {
            self.init(color: SKColor.clear, size: CGSize(width: 100, height: 100))
    
            // No need to do anything to myRing now, because my designated
            // initializer set it up completely.
    
            addChild(myRing)
            print("I'm on the screen")
    
            // Commented out because you didn't provide this property
            // or method in your question.
    //        explodeGroup = create_explosionActionGroup()
            }
    
        convenience init(color: SKColor, size: CGSize, position: CGPoint) {
            self.init(color: color, size: size)
            self.position = position
    
            // Commented out because you didn't provide this property
            // or method in your question.
    //        explodeGroup = create_explosionActionGroup()
        }
    
        required init?(coder aDecoder: NSCoder) {
            fatalError("init(coder:) has not been implemented")
        }
    
        private func configureMyRing() {
            myRing.path = CGPath(ellipseIn: CGRect(x: -ringSize / 2, y: -ringSize / 2, width: ringSize, height: ringSize), transform: nil)
            myRing.strokeColor = ringColor
            myRing.lineWidth = ringWidth
        }
    }