Tj3n
Tj3n

Reputation: 9923

iOS 13.2 MKTileOverlay occasionally won't render

I'm having an issue where in iOS 13.2 (probably also from iOS 13), loading offline map tile using MKTileOverlay occasionally won't be able to render, leaving the tile blank, there seems to be no issue with MKTileOverlay's subclass at all as it worked well in iOS 12 and below. I have 2 MKTileOverlay class (1 add grid and 1 load map tile file, default MKTileOverlay), both won't be able to load on that blank tile with default MKTileOverlayRenderer, other overlays seems to appear fine.

The issue seems to be resolved itself if I go to home screen and go back to the app, causing the tiles to reload. Is this a bug from iOS MapKit itself? Does anyone have temporary solution for this? Thank you.

Code for adding overlay:

let overlay = MKTileOverlay(urlTemplate: urlTemplate)
overlay.canReplaceMapContent = true
overlay.maximumZ = 19
mapView.insertOverlay(overlay, at: 0, level: .aboveLabels)

Renderer:

func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    if overlay is MKTileOverlay {
        let renderer = MKTileOverlayRenderer(tileOverlay: overlay as! MKTileOverlay)
        return renderer
    }

    return MKOverlayRenderer()
}

enter image description here

Upvotes: 3

Views: 1496

Answers (4)

Tj3n
Tj3n

Reputation: 9923

After quite a long time the issue seems to still happens with people so this is the workaround that I've been used for iOS 13 (it works fine at least for iOS 14 & 15), it will reload the map after user stopped moving the map for 1.5s:

    var mapReloadTask: DispatchWorkItem?

    func mapViewDidFinishRenderingMap(_ mapView: MKMapView, fullyRendered: Bool) {
        if #available(iOS 13, *), let tileOverlay = tileOverlay, let renderer = mapView.renderer(for: tileOverlay) as? MKTileOverlayRenderer {
            self.mapReloadTask?.cancel()
            let task = DispatchWorkItem {
                renderer.reloadData()
            }
            self.mapReloadTask = task
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.5, execute: task)
        }
    }

Upvotes: 0

FPP
FPP

Reputation: 348

I have developed a map app using this nice caching library for map tiles called MapCache. I face the same problems as mentioned here - occasionally tiles are not being rendered although the data source is perfectly ok and the tile is delivered via the sub-class of MKTileOverlayRenderer. Indeed, the proposal of tj3n calling renderer.reloadData() each time the rendering is finished at mapViewDidFinishRenderingMap(mapView:fullyRendered:) fixes the problem but its a little bit heavy since every little pan of the map will lead to a flickering update. But it gave me an idea. At least for me, if I notice a missing tile I intuitively try to zoom in or out with the 2-finger gesture. So, my workaround for this problem is to reloadData() if the zoom gesture is finished. That flickers as well but is not as annoying as the flickering after every pan.

    @objc func pinchGestureRecognized(_ gesture: UIPinchGestureRecognizer) {
        if gesture.state == .ended {                                // the gesture is finished
            overlayRenderer?.reloadData()                           // reload the renderer data for bug in MapKit of missing tiles
        }
    }

Hopefully Apple will fix that bug some time.

Upvotes: 0

Denis
Denis

Reputation: 31

It is clearly a MapKit issue/bug.

I've also open a feedback ticket since the 9th of December 2020.

The root of this issue is not very sure.

MapKit and specially MKTileOverlay always had/have some issues with "heavy" tiles like PNG 24bit. When the MKTileOverlay use PNG (heavy tiles), the tiles sometimes are flashing and the map is continuously reloading especially with wide screens (iPad pro etc..)

So, since the JPEG tiles are often lighter than PNG, JPEG can be a workaround.

BUT, this new iOS 13.2+ issue is not the same! Random tiles are not rendered. If you remove and readd the MKTileOverlay or call the reloadData method of MKTileOverlayRenderer, the missing tiles will be rendered and it will be other random tiles that which be missing.

The real solution of the issue is to open a feedback ticket: https://feedbackassistant.apple.com

Edit: I've just tried to replace my 8bit PNG by 85% JPEG on the very simple MKTileOverlay project sample i've sent to Apple in my ticket. Same issue... no improvement.

Edit 2: Loading the NSData into an UIImage then using UIImageRepresentationJPEG seems to do the trick... Ugly...

- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData * _Nullable, NSError * _Nullable))result
{
    NSString *tilePath = [self PATHForTilePath:path];
    NSData *data = nil;

    if (![[NSFileManager defaultManager] fileExistsAtPath:tilePath])
    {
        NSLog(@"Z%ld/%ld/%ld does not exist!", path.z, path.x, path.y);
    }
    else
    {
        NSLog(@"Z%ld/%ld/%ld exist", path.z, path.x, path.y);

        UIImage *image = [UIImage imageWithContentsOfFile:tilePath];
        data = UIImageJPEGRepresentation(image, 0.8);
        // Instead of: data = [NSData dataWithContentsOfFile:tilePath];

        if (data == nil)
        {
            NSLog(@"Error!!! Unable to read an existing file!");
        }
    }

    dispatch_async(dispatch_get_main_queue(), ^{
        result(data, nil);
    });
}

Upvotes: 3

Phil John
Phil John

Reputation: 1237

As I noted in a comment to the original question I was having the same problem, but it is now largely resolved, so I thought I'd post what worked for me.

The problem for me occurred in the following method

- (void)loadTileAtPath:(MKTileOverlayPath)path result:(void (^)(NSData * __nullable tileData, NSError * __nullable error))result{

where the custom tile would fail to load even when it was being presented with what appeared to be valid NSData.

I found that the problem was reduced if I used jpegs instead of pngs for my custom tiles, but it was only when I changed the way that I was handling the tile data did the problem largely go away. (I largely, because I still get the occasional unloaded tile, but I'd say it's 100x less often than I was getting them before).

The following method is my Xamarin.iOS implementation of it, but you should be able to see the principle for Swift or Objective C.

The key is the difference in the way the NSData is created. Instead of calling the UrlForTilePath method, I create a UIImage from the tile path and then use the UIImageJPEGRepresentation (AsJPEG in C#) to create the NSData.

   public override void LoadTileAtPath(MKTileOverlayPath path, MKTileOverlayLoadTileCompletionHandler result)
    {
        //I was using this prior to ios 13.2

        //NSUrl url = this.URLForTilePath(path);
        //NSData tileData = NSData.FromFile(url.AbsoluteString);
        //result(tileData, null);

        //Now I use this

        String folderPath = "tiles/" + path.Z + "/" + path.X + "/";
        String tilePath = NSBundle.MainBundle.PathForResource(path.Y.ToString(), "jpg", folderPath);
        String blankPath = NSBundle.MainBundle.PathForResource("tile", "jpg");

        try
        {
            //does the file exist?
            UIImage tile;
            if (File.Exists(tilePath))
            {
                tile = UIImage.FromFile(tilePath);
                if (tile == null)
                {
                    Console.WriteLine("Error Loading " + path.Z + " " + path.Y + " " + path.X);
                    //This may be redundant, as I'm not getting any errors here, even when the tile doesn't display
                }
            }
            else
            {
                tile = UIImage.FromFile(blankPath);
            }

            NSData tileData = tile.AsJPEG();
            result(tileData, null);
        }
        catch (Exception ex)
        {

        }
    }

Upvotes: 2

Related Questions