MKMapView is a great toolkit to customise and add a variety of things to a map on iOS, but one thing that I found lacking was an easy way to draw a multi-coloured line. Specifically, I wanted a way to draw a border around a line but the standard MKPolylineView component does not allow doing that. As digging around on GitHub, Google and StackOverflow did not yield any existing projects that allow doing so, I have a look for how to do it and the result is ASPolylineView - a simple drop-in replacement for an MKPolylineView.

MKPolylineView allows you to specify a single polyline based on coordinates and let you draw that line on a map. You can give it only a single strokeColor and lineWidth though. You might think that fillColor could help but the class does not support it and it is meant for filling the area encapsulated by the path.

My ASPolylineView is a replacement for MKPolylineView (not a subclass thereof but a subclass of MKOverlayPathView) that let’s you additionally specify a color for the border. What it then does is draw a thicker line of that color and afterwards add the regular line with the specified strokeColor.

To do so, one main thing that needs to be implemented is drawMapRect:zoomScale:inContext:.

- (void)drawMapRect:(MKMapRect)mapRect
          zoomScale:(MKZoomScale)zoomScale
          inContext:(CGContextRef)context
{
  UIColor *darker = [UIColor blackColor];
  CGFloat baseWidth = self.lineWidth / zoomScale;

  // draw the dark colour thicker
  CGContextAddPath(context, self.path);
  CGContextSetStrokeColorWithColor(context, darker.CGColor);
  CGContextSetLineWidth(context, baseWidth * 1.5);
  CGContextSetLineCap(context, self.lineCap);
  CGContextStrokePath(context);

  // now draw the stroke color with the regular width
  CGContextAddPath(context, self.path);
  CGContextSetStrokeColorWithColor(context, self.strokeColor.CGColor);
  CGContextSetLineWidth(context, baseWidth);
  CGContextSetLineCap(context, self.lineCap);
  CGContextStrokePath(context);

  [super drawMapRect:mapRect zoomScale:zoomScale inContext:context];
}

This is a standard application of the Quartz 2D framework to draw a path. The one interesting thing it does in addition is that the line width is made dependent on the zoom scale of the map.

By itself this code snippet does not work yet as you’ll find that self.path is not initialised. If it is not initialised, createPath is called automatically which does nothing by the default implementation. My subclass takes a MKPolyline on initialisation so what we need to do here is convert that into an CGPathRef which Quartz 2D can then draw.

- (void)createPath
{
  CGMutablePathRef path = CGPathCreateMutable();
  BOOL pathIsEmpty = YES;

  for (int i = 0; i < self.polyline.pointCount; i++) {
    CGPoint point = [self pointForMapPoint:self.polyline.points[i]];

    if (pathIsEmpty) {
      CGPathMoveToPoint(path, nil, point.x, point.y);
      pathIsEmpty = NO;
    } else {
      CGPathAddLineToPoint(path, nil, point.x, point.y);
    }
  }

  self.path = path;
}

This method makes sense of the helper method pointForMapPoint provided by MKOverlayPathView which converts the map points of the polyline to points that can be drawn on the map.

Works like a charm! Thanks at this point to the AIMapViewWrapper project which helped me get a better understanding of how MKOverlayPathView works.