kadrian
kadrian

Reputation: 5009

Updating and organizing overlays on an MKMapView

I want to do overlays on an MKMapView. So I created my custom object:

@interface Spot : NSObject

@property int spot_id;
@property CLLocationCoordinate2D coordinate;
@property float radius;
@property NSObject<MKOverlay> * overlay;

- (id) initWithSpotId:(int)spot_id position:(CLLocationCoordinate2D)coordinate andRadius:(float)radius;

@end

There is a backend, that constantly updates the Spots and delivers them to the phone. So I have to:

Is there a better way of keeping track of the overlays? Keeping an NSObject<MKoverlay> * reference attached to each spot and constantly reassigning it feels a bit strange to me.

Upvotes: 1

Views: 4015

Answers (1)

Rob
Rob

Reputation: 437372

It seems like there are three questions here:

  1. How to detect changes in spots in your server?

    The ugly way to do this would be iterate through your spots and see if the coordinate and/or radius changed. A little better would be to have the server update create, modify, and delete timestamps or some other identifier so the client would have the ability to retrieve all creations, modifications, and/or deletions since the last update. Best would be to marry that with some form of push notification, so the client would also be proactively notified of these changes.

    This question is difficult to answer in the abstract. It depends a lot upon the capabilities of your server and the nature of the database (e.g. how many "spots", how frequently do they change, etc.). This impacts both the client-server architecture, but also the client implementation.

  2. How to update the map when a spot changes?

    This is a far easier question. Insertions and deletions are easy. You just do addOverlay: (or, in iOS 7, addOverlay:level: and removeOverlay:. For updates, while it's inelegant, I think the easiest way is to just remove the old overlay and add it back in and viewForOverlay will take care of the user interface for you.

  3. What is the right structure of the Spot class?

    Just as a thought, but it seems duplicative to have your coordinate and radius properties, and then have a id<MKOverlay> overlay object too (because that's presumably a MKCircle with the same two properties). If your overlays are going to be MKCircle objects, it might be easier to just have a Spot class, itself, to conform to MKOverlay:

    @interface Spot : NSObject <MKOverlay>
    
    @property (nonatomic) int spot_id;
    @property (nonatomic) CLLocationCoordinate2D coordinate;
    @property (nonatomic) CLLocationDistance radius;
    @property (nonatomic, readonly) MKMapRect boundingMapRect;
    
    - (id) initWithSpotId:(int)spot_id position:(CLLocationCoordinate2D)coordinate andRadius:(CLLocationDistance)radius;
    
    @end
    

    Then, all you need to do is to implement boundingMapRect and intersectsMapRect:

    - (MKMapRect) boundingMapRect
    {
        MKMapPoint point = MKMapPointForCoordinate(self.coordinate);
        CLLocationDistance distance = self.radius * MKMetersPerMapPointAtLatitude(self.coordinate.latitude);
        MKMapRect rect = MKMapRectMake(point.x, point.y, distance * 2.0, distance * 2.0);
        rect = MKMapRectOffset(rect, -distance, -distance);
    
        return rect;
    }
    
    - (BOOL)intersectsMapRect:(MKMapRect)mapRect
    {
        return MKMapRectIntersectsRect(mapRect, [self boundingMapRect]);
    }
    

    You might want to double-check that boundingMapRect logic, but I think it's right.

    Then you can just add and remove Spot objects as overlays themselves. And all you need to do is to implement a viewForOverlay in your MKMapViewDelegate, for example, in iOS versions prior to 7, that would be:

    - (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
    {
        if ([overlay isKindOfClass:[Spot class]])
        {
            Spot *spot       = (id)overlay;
            MKCircle *circle = [MKCircle circleWithCenterCoordinate:spot.coordinate
                                                             radius:spot.radius];
    
            MKCircleView *overlayView = [[MKCircleView alloc] initWithCircle:circle];
            overlayView.fillColor     = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
            overlayView.strokeColor   = [[UIColor blueColor] colorWithAlphaComponent:0.7];
            overlayView.lineWidth     = 3.0;
            return overlayView;
        }
    
        return nil;
    }
    

    In iOS 7, that would be:

    - (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
    {
        if ([overlay isKindOfClass:[Spot class]])
        {
            Spot *spot       = (id)overlay;
            MKCircle *circle = [MKCircle circleWithCenterCoordinate:spot.coordinate
                                                             radius:spot.radius];
    
            MKCircleRenderer *renderer = [[MKCircleRenderer alloc] initWithCircle:circle];
            renderer.fillColor         = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
            renderer.strokeColor       = [[UIColor blueColor] colorWithAlphaComponent:0.7];
            renderer.lineWidth         = 3;
    
            return renderer;
        }
    
        return nil;
    }
    

    Another approach would be to define your Spot as:

    @interface Spot : NSObject <MKOverlay>
    
    @property (nonatomic) int spot_id;
    @property (nonatomic, strong) MKCircle *overlay;
    
    - (id) initWithSpotId:(int)spot_id position:(CLLocationCoordinate2D)coordinate andRadius:(CLLocationDistance)radius;
    

    And you could then just define boundingMapRect and coordinate return the appropriate values from the MKCircle (saving you from having to write your own):

    - (MKMapRect)boundingMapRect
    {
        return [self.circle boundingMapRect];
    }
    
    - (CLLocationCoordinate2D)coordinate
    {
        return [self.circle coordinate];
    }
    

    Clearly the init method would change:

    - (id) initWithSpotId:(int)spot_id position:(CLLocationCoordinate2D)coordinate andRadius:(CLLocationDistance)radius;
    {
        self = [super init];
        if (self) {
            _spot_id = spot_id;
            _circle = [MKCircle circleWithCenterCoordinate:coordinate radius:radius];
        }
        return self;
    }
    

    As would the viewForOverlay (iOS versions prior to 7.0) in the MKMapViewDelegate:

    - (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id<MKOverlay>)overlay
    {
        if ([overlay isKindOfClass:[SpotCircle class]])
        {
            SpotCircle *spot = (id)overlay;
    
            MKCircleView *overlayView = [[MKCircleView alloc] initWithCircle:spot.circle];
            overlayView.fillColor     = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
            overlayView.strokeColor   = [[UIColor blueColor] colorWithAlphaComponent:0.7];
            overlayView.lineWidth     = 3.0;
    
            return overlayView;
        }
    
        return nil;
    }
    

    In iOS 7, that would be:

    - (MKOverlayRenderer *)mapView:(MKMapView *)mapView rendererForOverlay:(id<MKOverlay>)overlay
    {
        if ([overlay isKindOfClass:[SpotCircle class]])
        {
            SpotCircle *spot = (id)overlay;
    
            MKCircleRenderer *renderer = [[MKCircleRenderer alloc] initWithCircle:spot.circle];
            renderer.fillColor         = [[UIColor cyanColor] colorWithAlphaComponent:0.2];
            renderer.strokeColor       = [[UIColor blueColor] colorWithAlphaComponent:0.7];
            renderer.lineWidth         = 3;
    
            return renderer;
        }
    
        return nil;
    }
    

I hope that helps.

Upvotes: 2

Related Questions