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 subclassinit()
calls the superclassinit()
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()
inb
will be called- then the
init()
ina
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
Can a Subclass Override a Function and Make More Restrictive Return
Callback Url Not Approved Despite Being Provided Twitter API
How Does Swift Memory Management Work
Cllocation Distancefromlocation (In Swift)
Protocol Extension Initializer Forcing to Call Self.Init
What Is the Markup Format for Documentation on the Parameters of a Block in Swift
How to Know Where Optional Chaining Is Breaking
Loop Over Multiple Uialertcontroller'S
Uiprogressview Progress Update Very Slow Within Alamofire (Async) Call
Swiftui: Using View Modifiers Between Different iOS Versions Without #Available
How to Observe Array Property Changes in Rxswift
Curl with Alamofire - Swift - Multipart/Form-Data
Sending an Email from Your App with an Image Attached in Swift
Swift Objc_Getassociatedobject Always Nil