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?
Convenience initializers are secondary, supporting initializers for a class. You can define a convenience initializer to call a designated initializer from the same class as the convenience initializer with some of the designated initializer's parameters set to default values.
Conversely, if you write a subclass initializer that matches a superclass convenience initializer, that superclass convenience initializer can never be called directly by your subclass, as per the rules described above in Initializer Delegation for Class Types.
An initializer is a special type of function that is used to create an object of a class or struct. In Swift, we use the init() method to create an initializer. For example, class Wall { ... // create an initializer init() { // perform initialization ... } }
Swift defines two kinds of initializers for class types to help ensure all stored properties receive an initial value. Designated initializers are the primary initializers for a class.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With