Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

What initializers should a MKAnnotationView subclass have in Swift?

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.)

like image 613
Kuba Suder Avatar asked Apr 09 '15 23:04

Kuba Suder


2 Answers

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.

like image 66
villy393 Avatar answered Oct 31 '22 01:10

villy393


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.

like image 2
nananta Avatar answered Oct 31 '22 00:10

nananta