Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

swift

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?

like image 942
Utku Avatar asked Nov 15 '16 19:11

Utku


1 Answers

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.

like image 75
Matthew Seaman Avatar answered Sep 25 '22 13:09

Matthew Seaman