11
votes

I'm creating a subclass of MKAnnotationView in my project. It needs to have two properties for storing subviews which I need to initialize somewhere at the beginning.

MKAnnotationView has one initializer listed in its documentation, initWithAnnotation:reuseIdentifier:, so I figured I'd simply override that:

class PulsatingDotMarker: MKAnnotationView {

    let innerCircle: UIView
    let outerCircle: UIView

    override init!(annotation: MKAnnotation!, reuseIdentifier: String!) {
        innerCircle = ...
        outerCircle = ...

        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    }

    ...
}

But this causes a runtime exception:

fatal error: use of unimplemented initializer 'init(frame:)' for class 'PulsatingDotMarker'

Ok, so I guess initWithAnnotation:reuseIdentifier: internally calls initWithFrame:, so it's probably that one that I should override instead. Let's try that:

class PulsatingDotMarker: MKAnnotationView {

    let innerCircle: UIView
    let outerCircle: UIView

    override init(frame: CGRect) {
        innerCircle = ...
        outerCircle = ...

        super.init(frame: frame)
    }

    ...
}

This however causes a compile error when creating the annotation view object:

Extra argument 'reuseIdentifier' in call

Hmm, so if I implement the (required) initializer initWithFrame:, it now loses the default initializer initWithAnnotation:reuseIdentifier:?

Maybe if I added an override of initWithAnnotation:reuseIdentifier: that just calls super it will be available again, will that work?

class PulsatingDotMarker: MKAnnotationView {

    let innerCircle: UIView
    let outerCircle: UIView

    init!(annotation: MKAnnotation!, reuseIdentifier: String!) {
        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    }

    override init(frame: CGRect) {
        innerCircle = ...
        outerCircle = ...

        super.init(frame: frame)
    }

    ...
}

Nope, still not good - compile error:

Property 'self.innerCircle' not initialized at super.init call

Ok, what if I had an initWithFrame:, but initialized the subviews in initWithAnnotation:reuseIdentifier:? (But then what if someone just calls initWithFrame: directly?...)

class PulsatingDotMarker: MKAnnotationView {

    let innerCircle: UIView
    let outerCircle: UIView

    init!(annotation: MKAnnotation!, reuseIdentifier: String!) {
        innerCircle = ...
        outerCircle = ...

        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
    }

    ...
}

Not surprisingly, Swift protects me from that by telling me:

Property 'self.innerCircle' not initialized at super.init call

(this time in initWithFrame:).

So what am I supposed to do? I can't create the subviews both here and there, right?

class PulsatingDotMarker: MKAnnotationView {

    let innerCircle: UIView
    let outerCircle: UIView

    init!(annotation: MKAnnotation!, reuseIdentifier: String!) {
        innerCircle = ...
        outerCircle = ...

        super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
    }

    override init(frame: CGRect) {
        innerCircle = ...
        outerCircle = ...

        super.init(frame: frame)
    }

    ...
}

Wrong again, this actually works - even though I'm assigning a constant property twice in the same object (!).

How should this be done properly?

(Note: the class also included a required initWithCoder: initializer that just calls fatalError from the first example, but the object is never created from a storyboard.)

3
What if the subviews are declared as var innerCircle: UIView!? That may avoid the "required init(xxx)" errors. - user467105
That's exactly what I ended up doing, but it sounds like a hack - marking a property as nullable and mutable even though it should be non-null and constant just to please the compiler... - Kuba Suder
I spent some time on similar problems, and made a test project where I think I managed to work out the cause (Objective C class calling a [self initWith...] method in its public init method), but unfortunately no solutions as yet - stackoverflow.com/questions/31161143/… - Rupert

3 Answers

8
votes

Unfortunately for MKAnnotationView forces you to implement init(frame: CGRect) which means you have to initialise all your instance variables in that method as well.

This article explains it a bit more

For variables that can only be initialised with passed in values you have to make those variables optional and set them to nil in the init(frame: CGRect).

The reason for this is that I suspect that MKAnnotationView is calling self.initWithFrame: rect in its objective-C init method. This is so that if a subclass overrides the initWithFrame:(CGRect) rect it will be called. However, this causes a problem in swift because if you declare a custom designated initialiser you do not inherit initialisers of the superclass. Therefore you have to implement the init(frame: CGRect) in your subclass.

I have had the same problem with UITableViewController. Its header looks to follow the same pattern. i.e two faliable designated initialisers.

It makes me very sad. But what can you do.

2
votes

For my app, the solution I chose is to declare the subview as an optional and instantiate it in initFrame...

var innerCircle: UIView?

Here is my code...

class EventAnnotationView: MKPinAnnotationView
{    
    static var REUSE_ID = "EventAnnotationView"

var imageView: UIImageView?

override init(frame: CGRect)
{
    super.init(frame: frame)

    // Create subview for custom images
    imageView = UIImageView(frame: CGRectMake(0, 0, 22, 22))

    ...

}

override init(annotation: MKAnnotation!, reuseIdentifier: String!)
{
    super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
}

required init(coder aDecoder: NSCoder)
{
    super.init(coder: aDecoder)
}
}

Feels like less of a hack :), but requires more code/work since the subview is an optional.

Hope this helps.

0
votes

There's clearly something actually broken in Swift 3 given the designated initializer isn't actually being called by iOS at run time.

I found the suggestion in the other answers don't compile (Tested on XCode 8.1 GM / iOS 10.1), but after various hacking around I found this combination works:

override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
     super.init(annotation: annotation, reuseIdentifier: reuseIdentifier);

     /* Your actual init code */
}

convenience init(frame: CGRect) {
    self.init(annotation: nil, reuseIdentifier: nil);
}