Jinghan Wang Jinghan Wang - 5 months ago 73
iOS Question

MKTileOverlay: How to reload a specific tile or region

I'm working on a map-based app. I used a

MKTileOverlay
to show some dynamic contents. Occasionally certain tiles need reloading when new information arrives.

I've tried to call
overlayRenderer.setNeedsDisplayInMapRect(mapRect)
in main thread but apparently it doesn't trigger a map redraw in my case. So far the only approach that works is
overlayRenderer.reloadData()
. However, this will cause the entire contents (instead of only specific region) to be reloaded and lead to flickering in the view, so this is not a option for me.

Can anyone give any advice on this? Thanks & Cheers.

Answer

Finally I figured it out. In fact, the overlay's not updating is not due to setNeedsDisplayInMapRect. After several checks I found that setNeedsDisplayInMapRect is actually causing a run of drawMapRect:zoomScale:inContext:, however, somehow a default redraw produces the same tile image. I guess this may due to some internal cache of MKTileOverlayRender.

The solution for me is to subclass the MKTileOverlayRender, and in the new class:

override func canDrawMapRect(mapRect: MKMapRect, zoomScale: MKZoomScale) -> Bool {

    let tilePath = self.tilePathForMapRect(mapRect, andZoomScale: zoomScale)
    let tilePathString = stringForTilePath(tilePath)
    if let _ = self.cache.objectForKey(tilePathString) {
        return true
    } else {
        let tileOverlay = self.overlay as! MKTileOverlay
        tileOverlay.loadTileAtPath(tilePath, result: {
            data, error in
            if error == nil && data != nil {
                if let image = UIImage(data: data!) {
                    self.cache.setObject(image, forKey: tilePathString)
                    self.setNeedsDisplayInMapRect(mapRect, zoomScale: zoomScale)
                }
            }
        })

        return false
    }
}

override func drawMapRect(mapRect: MKMapRect, zoomScale: MKZoomScale, inContext context: CGContext) {
    let tilePath = self.tilePathForMapRect(mapRect, andZoomScale: zoomScale)
    let tilePathString = stringForTilePath(tilePath)

    if let image = self.cache.objectForKey(tilePathString) as? UIImage {
        CGContextDrawImage(context, self.rectForMapRect(mapRect), image.CGImage)
    } else {
        super.setNeedsDisplayInMapRect(mapRect, zoomScale: zoomScale)
    }

    self.cache.removeObjectForKey(tilePathString)
}