Need help understanding interaction between convenience initializer and instance variable unwrapping in Swift

74 Views Asked by At

I'm pouring over the Swift documentation about initializers and unwrapping and I'm failing to grok something fundamental about the way this works.

Goal: I'd like to subclass SKShapeNode and use let reverse : Bool to define an instance variable that gets set only in the constructor and never need to be modified. That seems to be the correct way of defining such variables, but this blows up in the following code.

The following code works, i.e., it compiles, doesn't crash and gets me the desired behavior.

public class BeamedNotesNode: SKShapeNode {

    var notes : [Note]!
    var noteNodes : [NoteNode]?
    var beam : BeamNode?
    var childBeams : [BeamNode]?
    var reverse : Bool?

    convenience public init(
        withTicks notes: [Note],
        reverse: Bool = false)
    {
        self.init(rect: CGRect(x: 0, y: 0, width: 1, height: 1))

        self.notes = notes
        self.reverse = reverse
        ...

However, when I modify it to use let reverse : Bool, I get the error: Class 'BeamedNotesNode' has no initializers at compile time:

public class BeamedNotesNode: SKShapeNode {

    var notes : [Note]!
    var noteNodes : [NoteNode]?
    var beam : BeamNode?
    var childBeams : [BeamNode]?
    let reverse : Bool

    convenience public init(
        withTicks notes: [Note],
        reverse: Bool = false)
    {
        self.init(rect: CGRect(x: 0, y: 0, width: 1, height: 1))

        self.notes = notes
        self.reverse = reverse
        ...

It seems like I should be able to use let and avoid an ugly force-unwrapped instance variable. How do I achieve that here?

2

There are 2 best solutions below

0
On

The problem is that if you have a non-optional constant, you need to make sure it's initialized by the designated initializer, too.

The easiest solution if you want it to behave like a non-optional constant is to (a) provide a default value for the variable; but (b) make the setter private, i.e.

private(set) var reverse = false

I know this isn’t quite what you are looking for, but externally it behaves like a constant, but your convenience initializer can still override the value with whatever was supplied as a parameter.

0
On

This should work

public class BeamedNotesNode: SKShapeNode {

    var notes : [Note]!
    var noteNodes : [NoteNode]?
    var beam : BeamNode?
    var childBeams : [BeamNode]?
    let reverse : Bool

    public init(withTicks notes: [Note], reverse: Bool = false) {
        self.notes = notes
        self.reverse = reverse
        super.init()
    }

    public required init?(coder aDecoder: NSCoder) {
        self.reverse = false
        super.init(coder: aDecoder)
    }

}

What's going on?

There are a few rules about initializers involved here.

Fact #1

The moment you add a constant property to BeamedNotesNode, the compiler wants you to provide a designated initializer (not a convenience one) to populate that constant property.

Fact #2

But since you added a designated initializer, the designated initialisers from the superclass (SKShapeNode) are no longer available outside of your class. This makes your class no longer conform to NSCoding, so you need manually add another initializer

public required init?(coder aDecoder: NSCoder) {
    self.reverse = false
    super.init(coder: aDecoder)
}

And now the compiler is happy.

Considerations

I strongly recommend to avoid force unwrapped values in your code unless you don't have any alternative.

So this

var notes : [Note]!

should become

var notes : [Note]?