Search code examples
iosmapkitmkpolylinemkmultipolylinerenderer

MKMultiPolylineRenderer with attributes?


Is there any way to use attributes per polyline with a MKMultiPolylineRenderer?

Or do I have to create a different renderer even if the only difference is line width?

I tried sub-classing MKMultiPolylineRenderer, but I do not see anyway to tie attributes to the CGPath being drawn.

In the code below, draw(mapRect:zoomScale:in:) shows I have 1197 polylines, but strokePath(in:) is called only once per draw call.

If I could somehow tell what path number it was stroking I could bind attributes to the strokePath function. Since draw(mapRect:) only draws one path, I don't see how I can. If strokePath only provided an index I'd be all set.

I guess I could override the draw function entirely (and not call its super function), but I would be rendering all 1197 paths 1197 times since I don't know what path it is rendering per call.

class MyMultiPolylineRenderer: MKMultiPolylineRenderer {
    
    //Even though there are 1197 polylines, this is called once per draw(mapRect:)
    override func strokePath(_ path: CGPath, in context: CGContext) {
        super.strokePath(path, in: context)
        print("strokePath")
    }
    
    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
        
        //Causes a single call to strokePath
        super.draw(mapRect, zoomScale: zoomScale, in: context)

        var counter = 0
        for poly in self.multiPolyline.polylines {
            counter += 1
        }
        
        //polylines counter: 1197
        print("polylines counter: \(counter)")
    }
}

Solution

  • First we need a polyline that has an opinion about its color

    class ColoredPolyline: MKPolyline {
        var strokeColor: UIColor?
    }
    

    Let's create some test data: a straight blue line from Sagrada Família to Museu Blau and a straight yellow line from Sagrada Família to Museu Maritim:

    func createTestColoredMultiPolyline() -> MKMultiPolyline {
        let sagradaFamília = CLLocationCoordinate2D(latitude: 41.4035944, longitude: 2.1743616)
        let museuBlau = CLLocationCoordinate2D(latitude: 41.41103109073135, longitude: 2.221040725708008)
        let museuMaritim = CLLocationCoordinate2D(latitude: 41.37546408659406, longitude: 2.1759045124053955)
        
        let toBlauCoordinates = [sagradaFamília, museuBlau]
        let polylineToMuseuBlau = ColoredPolyline(coordinates: toBlauCoordinates, count: toBlauCoordinates.count)
        polylineToMuseuBlau.strokeColor = UIColor.blue
        
        let toMaritimCoordinates = [sagradaFamília, museuMaritim]
        let polylineToMuseuMaritim = ColoredPolyline(coordinates: toMaritimCoordinates, count: toMaritimCoordinates.count)
        polylineToMuseuMaritim.strokeColor = UIColor.yellow
        
        let testMultiPolyline = MKMultiPolyline([polylineToMuseuBlau, polylineToMuseuMaritim])
        
        return testMultiPolyline
    }
    

    Note that the sequence of those polylines matters, should you draw them on top of each other.

    now let's write a MKMultiPolylineRenderer that respects the color of ColoredPolyline (and uses color of MKMultiPolylineRenderer in other cases):

    class MultiColorMultipolylinerenderer: MKMultiPolylineRenderer {
        
        override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext) {
            let roadWidth = MKRoadWidthAtZoomScale(zoomScale)
            
            /// if polylines draw on top of each other the creator of self.multiPolyline is responsible for the correct sequence
            let polylines = self.multiPolyline.polylines
            
            context.saveGState()
    
            for polyline in polylines {
                // get coordinates of polyline
                let polylineMapPoints = polyline.coordinates.map { coord in
                    MKMapPoint(coord)
                }
                guard polylineMapPoints.count > 0 else {
                    continue
                }
    
                let coloredPolyline = polyline as? ColoredPolyline
                // usually all polylines in a MultiColorMultipolylinerenderer are rendered with the same self.strokeColor
                // but we let ColoredPolyline have its own opinion
                let polylineStrokeColor = coloredPolyline?.strokeColor ?? self.strokeColor ?? UIColor.label
                
    
                // stroke properties to context
                context.setBlendMode(CGBlendMode.exclusion)
                context.setStrokeColor(polylineStrokeColor.cgColor)
    
                if self.lineWidth > 0 {
                    context.setLineWidth(self.lineWidth*roadWidth)
                } else {
                    context.setLineWidth(roadWidth)
                }
                context.setLineJoin(self.lineJoin)
                context.setLineCap(self.lineCap)
                context.setMiterLimit(self.miterLimit)
                
                // create path
                context.move(to: self.point(for: polylineMapPoints[0]))
                for element in 1 ..< polyline.pointCount {
                    let point_ = self.point(for: polylineMapPoints[element])
                    context.addLine(to: point_)
                }
                
                // draw previously created path
                context.drawPath(using: .stroke)
                
            }
            
            context.restoreGState()
    
        }
        
    }
    

    that code uses a little helper:

    public extension MKMultiPoint {
        var coordinates: [CLLocationCoordinate2D] {
            var coords = [CLLocationCoordinate2D](repeating: kCLLocationCoordinate2DInvalid, count: pointCount)
            getCoordinates(&coords, range: NSRange(location: 0, length: pointCount))
            return coords
        }
    }
    

    This is how the test data looks in my tourist app:

    enter image description here