I am trying to subclass MKPolyline
. However the problem is, MKPolyline
does not have any designated initializers. Here is what I tried to do:
import MapKit
class MyPolyline: MKPolyline {
let locations: [Location]
init(locations: [Location]) {
self.locations = locations
var coordinates: [CLLocationCoordinate2D] = []
for location in locations {
coordinates.append(location.coordinate)
}
super.init(coordinates: coordinates, count: coordinates.count)
}
}
But I am getting Must call a designated initializer of the superclass 'MKPolyline'
error at the super.init
call. To the best of my knowledge, MKPolyline
does not have any designated initializers.
What should I do? How can I subclass a class which doesn't have any 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:
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.
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.
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