Stanislav Pankevich
Stanislav Pankevich

Reputation: 11368

The right way of setting up MapKit's delegate in a separate class

What is the proper way of setting up a separate delegate class for MapKit?

I have MapView class subclassing MKMapView and bare MapDelegate class conforming MKMapViewDelegate protocol having only one initializer method.

Here is the extract from MapView initialization method I use:

# MapView.m ...
@implementation MapView

- (id) initWithFrame:(CGRect)frame {
    if (self = [super initWithFrame:frame]) {

    // [self setShowsUserLocation:YES];

    [self setDelegate:[[MapDelegate alloc] initWithMapView:self]];

The only method MapDelegate class has is

# MapDelegate.m ...
- (id)initWithMapView:(MapView *)aMapView {
    self = [super init];
    self.mapView = aMapView;
    return self;
}

Having [self setShowsUserLocation:YES]; commented, all works fine - I see the map. If I uncomment this line, my application begins to crash.

What my MapDelegate class is missing?

UPDATE 1: if I don't use a separate class MapDelegate and set just setDelegate:self - all works.

UPDATE 2: Now I understand, that the problem with [self setDelegate:[[MapDelegate alloc] initWithMapView:self]]; string is that I need MapDelegate class to live longer than it does now (delegate property has weak attribute). If I do the following:

@property (strong) id delegateContainer;
....
[self setDelegateContainer:[[MapDelegate alloc] init]];
[self setDelegate:self.delegateContainer];

...it works! Is there a better way of retaining MapDelegate life cycle along with the one of MKMapView?

Thanks!

Upvotes: 2

Views: 647

Answers (2)

Stanislav Pankevich
Stanislav Pankevich

Reputation: 11368

After waiting enough for any answers that could appear here and ensuring original problematic behavior twice more times, I am posting my own answer based on the second update from the question:

The problem with [self setDelegate:[[MapDelegate alloc] initWithMapView:self]]; string is that MapDelegate class should be able to be kept alive outside of the scope of question's initWithFrame method because delegate property has weak attribute. The possible solution is to create an instance variable serving as a container for a delegate class, for example:

@property (strong) id delegateClass;
....
[self setDelegateClass:[[MapDelegate alloc] init]];
[self setDelegate:self.delegateClass];

This solves the original problem.

LATER UPDATE

Though it is possible to set MKMapView's delegate in a separate class, I now realize that such model should not be used:

Currently I always prefer to use my controllers (i.e. controller layer in MVC in general) as delegates for all of my View layer classes (map view, scroll view, text fields): controller level is the place where all the delegates of different views can meet - all situated in controller layer, they can easily interact with each other and share their logic with the general logic of your controller.

On the other hand, if you setup your delegate in a separate class, you will need to take additional steps to connect your separate delegate with some controller, so it could interact with a rest part of your logic - this work have always led me to adding additional and messy pieces of code.

Shortly: do not use separate classes for delegates (at least view classes delegates provided by Apple), use some common places like controllers (fx for views like UIScrollView, MKMapView, UITableView or models like NSURLConnection).

Upvotes: 2

nevan king
nevan king

Reputation: 113747

I think viewDidLoad would be a better place to set up the map view. It's just a guess, but perhaps the crash is due to the view not being loaded yet.

Of course subclassing MKMapView isn't recommended at all. You would generally put your map as a subview, and set the main view to be the delegate. From the docs:

Although you should not subclass the MKMapView class itself, you can get information about the map view’s behavior by providing a delegate object.

Finally, if you really want to have a separate delegate class, you don't need to set its mapView, as all delegate methods pass the map as an argument.

Upvotes: 0

Related Questions