Adding Convenience Initializers in Swift Subclass
Asked Answered
I

4

24

As a learning exercise I am trying to implement a subclass of SKShapeNode that provides a new convenience initializer that takes a number and constructs a ShapeNode that is a square of number width and height.

According to the Swift Book:

Rule 1

If your subclass doesn’t define any designated initializers, it automatically inherits all of its superclass designated initializers.

Rule 2

If your subclass provides an implementation of all of its superclass designated initializers—either by inheriting them as per rule 1, or by providing a custom implementation as part of its definition—then it automatically inherits all of the superclass convenience initializers.”

However, the following class doesn't work:

class MyShapeNode : SKShapeNode {
    convenience init(squareOfSize value: CGFloat) {
        self.init(rectOfSize: CGSizeMake(value, value))
    }
}

Instead I get:

Playground execution failed: error: <REPL>:34:9: error: use of 'self' in delegating initializer before self.init is called
        self.init(rectOfSize: CGSizeMake(value, value))
    ^
<REPL>:34:14: error: use of 'self' in delegating initializer before self.init is called
    self.init(rectOfSize: CGSizeMake(value, value))
         ^
<REPL>:35:5: error: self.init isn't called on all paths in delegating initializer
}

My understanding is that MyShapeNode should inherit all of SKShapeNode's convenience initializers because I am not implementing any of my own designated initializers, and because my convenience initializer is calling init(rectOfSize), another convenience initializer, this should work. What am I doing wrong?

Imbecility answered 23/6, 2014 at 18:46 Comment(4)
did you try it with super instead of self ?Macdonald
@Macdonald yep, super doesn't work because convenience initializers must delegate to an initializer on the current class. I can call a designated initializer, like self.init() but I want to re-use the behavior in the existing init(rectOfSize) method of SKShapeNode.Imbecility
for pure swift this is working. so it might be a problem with ObjC Swift IntegrationLussier
I believe the problem here is that there's no "initWith" ObjC methods, so there's no automatic designated initializer. The class methods "fooWithBar" are interpreted by the Swift-ObjC translation as convenience initializers. In the case of SKShapeNode (and others, such as UIAlertController), that leaves the ObjC implementation with no designated initializers and only convenience initializers which means what you're trying to do will fail. This is almost certainly a bug, and relating to a no ability on the ObjC end to declare convenience vs. designated that Swift can use.Adam
P
15

My understanding of Initializer Inheritance is the same as yours, and I think we are both well aligned with what the book states. I don't think it's an interpretation issue or a misunderstanding of the stated rules. That said, I don't think you're doing anything wrong.

I tested the following in a Playground and it works as expected:

class RectShape: NSObject {
    var size = CGSize(width: 0, height: 0)
    convenience init(rectOfSize size: CGSize) {
        self.init()
        self.size = size
    }
}

class SquareShape: RectShape {
    convenience init(squareOfSize size: CGFloat) {
        self.init(rectOfSize: CGSize(width: size, height: size))
    }
}

RectShape inherits from NSObject and doesn't define any designated initializers. Thus, as per Rule 1, it inherits all of NSObject's designated initializers. The convenience initializer I provided in the implementation correctly delegates to a designated initializer, prior to doing the setup for the intance.

SquareShape inherits from RectShape, doesn't provide a designated initializer and, again, as per Rule 1, inherits all of SquareShape's designated initializers. As per Rule 2, it also inherits the convenience initializer defined in RectShape. Finally, the convenience initializer defined in SquareShape properly delegates across to the inherited convenience initializer, which in turn delegates to the inherited designated initializer.

So, given the fact you're doing nothing wrong and that my example works as expected, I am extrapolating the following hypothesis:

Since SKShapeNode is written in Objective-C, the rule which states that "every convenience initializer must call another initializer from the same class" is not enforced by the language. So, maybe the convenience initializer for SKShapeNode doesn't actually call a designated initializer. Hence, even though the subclass MyShapeNode inherits the convenience initializers as expected, they don't properly delegate to the inherited designated initializer.

But, again, it's only a hypothesis. All I can confirm is that the mechanics works as expected on the two classes I created myself.

Photocell answered 23/6, 2014 at 19:30 Comment(0)
F
26

There are two problems here:

  • SKShapeNode has only one designated initializer: init(). This means that we cannot get out of our initializer without calling init().

  • SKShapeNode has a property path declared as CGPath!. This means that we don't want to get out of our initializer without somehow initializing the path.

The combination of those two things is the source of the issue. In a nutshell, SKShapeNode is incorrectly written. It has a property path that must be initialized; therefore it should have a designated initializer that sets the path. But it doesn't (all of its path-setting initializers are convenience initializers). That's the bug. Putting it another way, the source of the problem is that, convenience or not, the shapeNodeWith... methods are not really initializers at all.

You can, nevertheless, do what you want to do — write a convenience initializer without being forced to write any other initializers — by satisfying both requirements in that order, i.e. by writing it like this:

class MyShapeNode : SKShapeNode {
    convenience init(squareOfSize value: CGFloat) {
        self.init()
        self.init(rectOfSize: CGSizeMake(value, value))
    }
}

It looks illegal, but it isn't. Once we've called self.init(), we've satisfied the first requirement, and we are now free to refer to self (we no longer get the "use of 'self' in delegating initializer before self.init is called" error) and satisfy the second requirement.

Finally answered 2/7, 2014 at 16:54 Comment(8)
"It looks illegal". That putting it mildly. It's looks morally wrong, as in "set it on fire out of disgust," morally wrong. Spent two days beating my head against this wall. This solution would never have occurred to me, ever. Having two separate initializers not visibly connected and just hanging out in space like feels creepy. I can confirm it works. You sir are a genius. A warped one, apparently, but genius none the less.Flanigan
@Flanigan "Warped"???? That kind of flattery will get you nowhere. But the check is in the mail.Finally
While that does seem to satisfy the compiler's bizarre desires for initialization, I don't think it has to do with the path property. CGPath! is still an optional, albeit a forced-unwrapped optional, and it's perfectly acceptable to initialize an instance with a nil forced-unwrapped optional (although arguably an unsafe practice).Imbecility
Thanks matt! I was banging my head against the wall on this too!Kania
I think this is all close to the issue I'm running into, but the above code doesn't seem to work in a playground in 6.3.2Hypsometer
Calling init twice in this way seems to cause a memory leak. Memory Graph View in Xcode 8 shows a runtime warning for my subclass that uses this approach.Pittance
@Pittance I'm pretty sure that leak is a false alarm and a known bug in the memory graph, and is perhaps fixed in 8.2.Finally
@Finally ah, yes, I've verified that deinit is called the expected number of times. Still a bug then in Xcode 8.2Pittance
P
15

My understanding of Initializer Inheritance is the same as yours, and I think we are both well aligned with what the book states. I don't think it's an interpretation issue or a misunderstanding of the stated rules. That said, I don't think you're doing anything wrong.

I tested the following in a Playground and it works as expected:

class RectShape: NSObject {
    var size = CGSize(width: 0, height: 0)
    convenience init(rectOfSize size: CGSize) {
        self.init()
        self.size = size
    }
}

class SquareShape: RectShape {
    convenience init(squareOfSize size: CGFloat) {
        self.init(rectOfSize: CGSize(width: size, height: size))
    }
}

RectShape inherits from NSObject and doesn't define any designated initializers. Thus, as per Rule 1, it inherits all of NSObject's designated initializers. The convenience initializer I provided in the implementation correctly delegates to a designated initializer, prior to doing the setup for the intance.

SquareShape inherits from RectShape, doesn't provide a designated initializer and, again, as per Rule 1, inherits all of SquareShape's designated initializers. As per Rule 2, it also inherits the convenience initializer defined in RectShape. Finally, the convenience initializer defined in SquareShape properly delegates across to the inherited convenience initializer, which in turn delegates to the inherited designated initializer.

So, given the fact you're doing nothing wrong and that my example works as expected, I am extrapolating the following hypothesis:

Since SKShapeNode is written in Objective-C, the rule which states that "every convenience initializer must call another initializer from the same class" is not enforced by the language. So, maybe the convenience initializer for SKShapeNode doesn't actually call a designated initializer. Hence, even though the subclass MyShapeNode inherits the convenience initializers as expected, they don't properly delegate to the inherited designated initializer.

But, again, it's only a hypothesis. All I can confirm is that the mechanics works as expected on the two classes I created myself.

Photocell answered 23/6, 2014 at 19:30 Comment(0)
C
6

Building on Matt's answer, we had to include an additional function, or else the compiler complained about invoking an initializer with no arguments.

Here's what worked to subclass SKShapeNode:

class CircleNode : SKShapeNode {

    override init() {
        super.init()
    }

    convenience init(width: CGFloat, point: CGPoint) {
        self.init()
        self.init(circleOfRadius: width/2)
        // Do stuff
     }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
Ceres answered 22/2, 2017 at 9:9 Comment(0)
H
2

Good news from 2019! I can report that I now have a SKShape subclass that has the following three initializers:

    override init() {
    super.init()
}

convenience init(width: CGFloat, point: CGPoint) {
    self.init(circleOfRadius: width/2)
    self.fillColor = .green
    self.position = point
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

and that behaves exactly as expected: when you call the convenience initializer, you get green dots in the desired position. (The double calling of init() as described by @matt and @Crashalot, on the other hand, now results in an error).

I'd prefer to have the ability to modify SKShapeNodes in the .sks scene editor, but you can't have everything. YET.

Homochromatic answered 1/10, 2019 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.