How to Subclass a Class Which Doesn't Have Any Designated Initializers

How to subclass a class which doesn't have any designated initializers?

Designated Initializers

As you seem to know, designated initializers provide a way of creating an object from a set of parameters. Every Swift object must have at least one designated initializer, although in certain cases Swift may provide them by default.

In the case of a class, a default initializer will be provided iff all stored properties of the class provide default values in their declarations. Otherwise, a designated initializer must be provided by the class creator. Even if a designated init is provided explicitly, however, it is not required to have an access control level that would make it accessible to you.

So MKPolyline surely has at least one designated init, but it may not be visible to you. This makes subclassing effectively impossible, however there are alternatives.

Two alternatives immediately come to mind:

Class Extensions and Convenience Initializers

One great feature of Swift is that classes may be extended even if the original class was defined in another module, even a third party one.

The issue you are running into is that MKPolyline defines convenience inits but no public designated ones. This is an issue because Swift has a rule that a designated init must call a designated init of its immediate superclass. Because of this rule, even a designated init will not work, even in an extension. Fortunately, Swift has convenience initializers.

Convenience inits are just initializers that work by eventually calling designated inits within the same class. They do not delegate up or down, but sideways, so to speak. Unlike a designated init, a convenience init may call either a designated init or another convenience init, as long as it is within the same class.

Using this knowledge, we could create an extension to MKPolyline that declares a convenience init which would call one of the other convenience inits. We can do this because inside of an extension it is just like you are in the original class itself, so this satisfies the same-class requirement of convenience inits.

Basically, you would just have an extension with a convenience init that would take an array of Location, convert them to coordinates, and pass them to the convenience init MKPolyline already defines.

If you still want to hold the array of locations as a stored property, we run into another problem because extensions may not declare stored properties. We can get around this by making locations a computed property that simply reads from the already-existing getCoordinates method.

Here's the code:

extension MKPolyline {

var locations: [Location] {
guard pointCount > 0 else { return [] }

let defaultCoordinate = CLLocationCoordinate2D(latitude: 0.0, longitude: 0.0)
var coordinates = [CLLocationCoordinate2D](repeating: defaultCoordinate, count: pointCount)
getCoordinates(&coordinates, range: NSRange(location: 0, length: pointCount))

// Assuming Location has an init that takes in a coordinate:
return coordinates.map({ Location(coordinate: $0) })
}

convenience init(locations: [Location]) {

let coordinates = locations.map({ $0.coordinate })

self.init(coordinates: coordinates, count: coordinates.count)
}

}

Here's what's going on. At the bottom we have a convenience init very similar to what you already did except it calls a convenience init on self since we're not in a subclass. I also used map as a simpler way of pulling the coordinates out of Location.

Lastly, we have a computed locations property that uses the getCoordinates method behind the scenes. The implementation I have provided may look odd, but it is necessary because the getCoordinates function is Objective-C–based and uses UnsafeMutablePointer when imported to Swift. You therefore need to first declare a mutable array of CLLocationCoordinate2D with exact length, and then pass it to getCoordinates, which will fill the passed array within the range specified by the range parameter. The & before the coordinates parameter tells Swift that it is an inout parameter and may be mutated by the function.

If, however, you need locations to be a stored property in order to accommodate a more complex Location object, you'll probably need to go with the second option described below, since extensions may not have stored properties.

Wrapper Class

This solution doesn't feel as 'Swifty' as the previous, but it is the only one I know of that would let you have a stored property. Basically, you would just define a new class that would hold an underlying MKPolyline instance:

class MyPolyline {

let underlyingPolyline: MKPolyline

let locations: [Location]

init(locations: [Location]) {
self.locations = locations

let coordinates = locations.map { $0.coordinate }
self.underlyingPolyline = MKPolyline(coordinates: coordinates, count: coordinates.count)
}

}

The downside to this approach is that anytime you want to use MyPolyline as an MKPolyline, you will need to use myPolyline.underlyingPolyline to retrieve the instance. The only way around this that I know of is to use the method described by the accepted answer to this question in order to bridge your type to MKPolyline, but this uses a protocol that is undocumented and therefore may not be accepted by Apple.

How to initialize a subclass when no superclass init fits

The problem you're having calling super.init() is because that initializer is actually a convenience initializer, and subclasses must call a designated initializer. Most UIKit/AppKit classes that have both a designated initializer that accepts an NSRect and a convenience initializer with no parameters simply chain to the designated one with .zero for the NSRect. Paulw11's comments allude to this when he suggests calling it explicitly with .zero.

What is the designated initializer for a MKPolygon in Swift4?

The real answer to this specific issue is that you can subclass them, but your subclass must not require using its own initializer. Instead you must continue to use the convenience initializers in the super classes. Those initializers must be called otherwise MapKit won't render the data.

For example with MKPolyline:

let mine = MyPolyline(coordinates: c, count c.count)
mine.other = ...
mine.foobar = ...

class MyPolyline: MKPolyline {
var foobar: [Int] = []
var other: [Double] = []
}

You might think you could add your own initializer and simply override methods like points and pointCount (similar to what you would for NSString for example), but MapKit still won't render the data unless its own "convenience"-marked initializers were called.

Override designated initializer of superclass

@justin is basically on the point.

Methods in Objective-C are inherited. That means if the superclass has an initializer method (initializers are just methods), and your subclass does not override it, then your subclass will inherit that superclass's initializer method. That means that people can always call that superclass's initializer on an object of your subclass (basic consequence of inheritance and subtype polymorphism). But that might not be what you expected. The superclass's initializer might not do all the initialization that your class needs.

That's why you should override the superclass's initializer. If you don't want people to use that initializer on an object of your class, you should throw an exception in that initializer. Otherwise, you should override it to do any appropriate initialization for your class.

How to know super class designated initializers?

UILongPressGestureRecognizer inherits from UIGestureRecognizer. The designated initializer for UIGestureRecognizer is public init(target: AnyObject?, action: Selector).

Override init(target: AnyObject?, action: Selector) instead of init().

I found the designated initializer by jumping to the definition of UILongPressGestureRecognizer. I didn't see an initializer there, but I did see that it was a subclass of UIGestureRecognizer. Stepping into the UIGestureRecognizer declaration revealed the public initializer.

This information is also available in the Apple API docs. The documentation will say Designated Initializer next to the designated initializer.

UIGestureRecognizer Documentation

Can a subclass not inherit the superclass' initializer and when do we use the required initializer?

Try and think about it in terms of what initialization does for an object. It sets values to parameters that do not have values set to them yet that need values set before use. See: https://docs.swift.org/swift-book/LanguageGuide/Initialization.html#ID228. So each class needs to have a way to initialize it and if necessary deal with setting variables or passing that responsibility along the subclass chain. The required init() function should be used as a unique case where somewhere in the initialization chain a special property is computed/set in required init() of a super class that makes it a requirement to call required init() in a subclass/subclasses of it. You do not need to write override required in this case.

Why is the superclass designated initializer getting called by default?

Why the init() of SuperClass is getting called? Does a subclass init() calls the superclass init() by default?

Basically, yes.

If all the rules say that you should say super.init() and you don't say it, it is called for you.

I don't like this behavior; it is poorly documented, and besides, secretly doing stuff for you seems against the spirit of Swift. But I filed a bug against it long ago and was told it was intended behavior.

Does a swift subclass *always* have to call super.init()

No, you don't have to.

Assume you have the following classes.

class a {
let name: String

init() {
name = "james"
}
}

class b: a {
let title: String

override init() {
title = "supervisor"
}
}

If you instantiate a variable with

let myVar = b()

Then,

  • override init() in b will be called
  • then the init() in a will be called

Even though you didn't explicitly call super.init().


This has been confirmed by Chris Laettner on the swift-user's email list.
It kicks in when your super class has a single designated initializer with a zero-argument init. This is why you don’t have to call super.init() when deriving from NSObject.

*Thanks to Wingzero's comment below

Adding Convenience Initializers in Swift Subclass

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.



Related Topics



Leave a reply



Submit