Search code examples
iosiphoneswiftmapkitmkoverlay

Properly subclassing MKOverlayRenderer


I'm trying to modify the path of a MKPolyline at runtime to avoid it overlaps with another one.

I already managed to get all the overlapping points and what I'm trying to do is in the func createPath() of the MKPolylineRenderer add an offset to does points so, theoretically, it should draw the same path with the little offset I'm adding and it shouldn't overlap anymore, but sadly, this is not happening and the Polyline is drawn in the same way like nothing changed.

I first tried to do this after the addPolyline() function but I read that once you do that, the one way to redraw a Polyline is by removing it and adding it again, so I decided, for testing purposes, to do all of this before adding the Polyline so when I finally add it to the map, it will already have the information about the overlapping points, but this didn't worked either.

Hypothesis:

1. It has something to do that the map works on different threads and the changes are not reflected because of that. This is ok. It should be this way to optimise the rendering.

2. The correct way to accomplish this is not in the createPath() function. Indeed it isn't

  1. I should apply a transform in the draw() function of the renderer. This is it

This is the createPath() function

override func createPath()
{
   let poly = polyline as! TransportPolyline

   switch poly.id
    {
    case 1:
        let newPath = CGMutablePath()

        for index in 0...poly.pointCount
        {
            let point = poly.points()[index]
            let predicate = { MKMapPointEqualToPoint($0, poly.points()[index]) }
            //This is the offset I should apply
            let offset: CGFloat = overlapsAtPoints.contains(predicate) ? 100000.0 : 0.0
            //I tried to use a transform as well, but the result was the same
            var transform = CGAffineTransform(translationX: offset, y: offset)

            if index == 0
            {
                //Here I add the offset and/or the transform without success
                newPath.moveTo(&transform, x: CGFloat(point.x) + offset, y: CGFloat(point.y) + offset)
            }
            else
            {
                //Here as well
                newPath.addLineTo(&transform, x: CGFloat(point.x) + offset, y: CGFloat(point.y) + offset)
            }
        }

        //Set the new path to the Renderer path property
        self.path = newPath
    default: break
    }
}

And this is the draw() function

override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
    {
        let poly = polyline as! TransportPolyline

        guard poly.id == 1 else {
            super.draw(mapRect, zoomScale: zoomScale, in: context)
            return
        }

        //If I apply this the Polyline does move, obviously it move all the Path and not only the segments I want.
        context.translate(x: 1000, y: 1000)

        super.draw(mapRect, zoomScale: zoomScale, in: context)
    }

Any suggestions are much appreciated.

UPDATE:

I found out that the problem might be in how I'm drawing the context in the draw method.

The documentation says:

The default implementation of this method does nothing. Subclasses are expected to override this method and use it to draw the overlay’s contents.

so by calling super.draw() I'm not doing anything.

Any ideas on how to properly override this method? Also taking into consideration this:

To improve drawing performance, the map view may divide your overlay into multiple tiles and render each one on a separate thread. Your implementation of this method must therefore be capable of safely running from multiple threads simultaneously. In addition, you should avoid drawing the entire contents of the overlay each time this method is called. Instead, always take the mapRect parameter into consideration and avoid drawing content outside that rectangle.


Solution

  • So basically I was on the right track but using the wrong tools. The actual way to accomplish this is by overriding the draw() function in you MKPolylineRenderer subclass.

    override func draw(_ mapRect: MKMapRect, zoomScale: MKZoomScale, in context: CGContext)
    {
       //First validate that the Rect you are asked to draw in actually 
         has some content. See last quote above.
       let theMapRect: MKMapRect = self.overlay.boundingMapRect;
    
       guard (MKMapRectIntersectsRect(mapRect, theMapRect)) || self.path != nil else {
            return
       }
    
       //Do some logic if needed.
    
       //Create and draw your path
       let path = CGMutablePath()
       path.moveTo(nil, x: self.path.currentPoint.x, y: self.path.currentPoint.y)
       path.addLines(nil, between: remainingPoints, count: remainingPoints.count)
    
       context.addPath(path)
    
       //Customise it
       context.setStrokeColor(strokeColor!.cgColor)
       context.setLineWidth((lineWidth + CGFloat(0.0)) / zoomScale)
    
       //And apply it 
       context.strokePath()
    }
    

    By doing this I was able to successfully draw the path I wanted for each overlay without any troubles.