I am using MKTileOverlays to display a custom map. I have .canReplaceMapContent
set to true since I don't need or want to display the standard map tiles. But every time the zoom level changes, the entire screen flashes gray (the background color of the MKMapView
). I thought if I could set the map view background to transparent, I could place a screenshot behind the map view before the view is zoomed and remove it after the view is rendered. The view snapshot would also have to zoom in with the view, but it seems doable.
Through layer sniffing, I found that the gray background (RGB 21,21,21) comes from a VKMapView
layer in this hierarchy:
Layer: CALayer
Layer: CALayer
Layer: _MKMapLayerHostingLayer
Layer: VKMapView // Has background color RGB(21,21,21)
Layer: CAMetalLayer
I've tried several approaches to modify this, such as setting the VKMapView
layer's background to .clear
:
if let firstLayer = layer.sublayers?.first,
let secondLayer = firstLayer.sublayers?.first,
let thirdLayer = secondLayer.sublayers?.first,
let fourthLayer = thirdLayer.sublayers?.first {
fourthLayer.backgroundColor = UIColor.clear.cgColor
fourthLayer.isOpaque = false
if let metalLayer = fourthLayer.sublayers?.first as? CAMetalLayer {
metalLayer.backgroundColor = UIColor.clear.cgColor
metalLayer.isOpaque = false
metalLayer.pixelFormat = .bgra8Unorm_srgb
}
}
Setting the layer's opacity to 0 confirms it is the correct layer, but something else must be setting the background color later in the view life cycle. Of course, with the alpha set to 0, it won't display any map content.
I tried setting these properties at various lifecycle points (init
, viewDidLoad
, viewDidAppear
, etc.), but nothing worked.
Ideally, I'd like MapKit to either:
• Keep showing the previous zoom level's tiles until new ones are ready
• Allow the background to be transparent or customizable
• Or provide some other way to prevent the flash
Has anyone managed a work around to the flashing? It just destroys what would be a nice user experience. I filed a bug for this many months ago with Feedback Assistant FB13989005 but has been ignored.
The Apple sample code "MapKitOverlays" recreates the following issues.
The "Load tiles from Server" and "Customized Tile Loading" examples flicker, even after the tiles have been loaded and cached. I verified that the issue occurs even when URLSession is using the cached tiles by adding the delegate function urlSession(_ , task, didFinishCollecting metrics:)
.
When using local tiles, the entire screen flashes blank for a frame or so every time the zoom level changes one level, even after they’ve been loaded. I’m using the attached following for the overlay. Note it only flashes at each zoom level only when the flyover map modes are not used.
For unknown reasons, using the flyover modes is much better. The problem is that when using flyover modes with .canReplaceContent
is set to true now causes a severe bug that causes the label text to wander great distances from the features they label. Apple has acknowledged but said they can't say if or when it will ever be fixed.
extension MapViewController {
func configureLocalTileOverlay() {
let tileDirectoryName = "localTiles"
guard let resourcePath = Bundle.main.resourcePath else { return }
let localPath = "file://\(resourcePath)/\(tileDirectoryName)/{z}/{x}/{y}.png"
let tileOverlay = MKTileOverlay(urlTemplate: localPath)
tileOverlay.canReplaceMapContent = true
tileOverlay.tileSize = CGSize(width: 512, height: 512)
tileOverlay.minimumZ = 2
tileOverlay.maximumZ = 14
mapView.addOverlay(tileOverlay, level: .aboveRoads)
}
}
I think I've found a solution to this. It seems like the issue is with MKTileOverlayRenderer
and how + when it draws.
I initially assumed that it was just not using the cache correctly, since I saw this even when bouncing back and forth between two levels where all the tiles should have been cached. So I implemented my own subclass of MKTileOverlay
which overrode loadTile(at:result:)
. It wasn't the cache! I even tried adding my own cache and logging in loadTile(at:result:)
, but despite getting all hits, the flicker was still there!
Then I implemented an MKTileOverlayRenderer
subclass that overrode canDraw(_:zoomScale:)
. This turned out not to work on its own, but is part of the larger solution.
Te get rid of the flash, you need to tell MapKit that you actually CAN draw even if the image isn't available yet (does require subclassing MKOverlayRenderer
). Then you need to introduce a way to query the cache immediately within the draw method of your MKOverlayRenderer
. Finally, you need to implement a draw method that can query the cache quickly and draw right then, OR kick off an async task.
That all comes out to a few hundred lines of code + a lengthy blog post explaining it.
I've published the solution I came up with in a Swift Package. It includes the overlay renderer implementation as well as a protocol for your MKOverlay
subclass to implement. The protocol relies on the overlay being able to access the cache directly, which seems like a reasonable thing in this scenario to speed up the rendering. This approach does remove the flicker.
A related problem you didn't mention, but which becomes obvious after fixing the first problem is that MapKit won't ever "overzoom" across a level boundary. The Swift package I wrote also fixes that, at least when zooming in, by slicing up the cached tile from the previous zoom level and using that as a stand-in.