iosobjective-cmultithreadingmapkitmkmapsnapshotter

How do I use MKMapSnapshotter in the background?


I've got an app that is supposed to fetch objects in the background and use their location data to generate a map snapshot for them. Naturally I tried MKMapSnapshotter.

It turns out (after weeks of being confused about black map snapshots) that this tool only seems to work when called from the main thread like so:

dispatch_async(dispatch_get_main_queue(), ^{
   MKMapSnapshotter *snapshotter = [[MKMapSnapshotter alloc] initWithOptions:options];
        [snapshotter startWithQueue:dispatch_get_main_queue() completionHandler:^(MKMapSnapshot * _Nullable snapshot, NSError * _Nullable error) {
       //Use image here. Image would be completely black if not for the first line of code specifying main thread.
   }];
});

Is this a framework bug?

Problem: This only runs when my app is in the foreground.


Solution

  • This was a bit complicated for the app I'm working on since there are many calls to download a set of map tiles for multiple zoom levels, so the code below may be a bit more complex than you need (but shows that queues work for snapshotting). For example, I needed a dispatchSemaphore to avoid queueing up hundreds to thousands of concurrent snapshots - this limits them to about 25 concurrent snapshots being captured on the thread.

    Also, I am doing this in Swift 3 so there may be changes in GCD that let me do it while presenting issues for you.

    The logic here is to get all of the requests started in the processQueue while the main queue remains unblocked so the UI stays active. Then, as up to 25 of the requests pass through the semaphore gate at any one time, they enter the snapshotQueue via the snapshotter.start call. When one snapshot finishes, another is started up until the processQueue is empty.

    unowned let myself = self   // Avoid captures in closure
    
    let processQueue = DispatchQueue(label: "processQueue", qos: .userInitiated)
    let snapshotQueue = DispatchQueue(label: "snapshotQueue")
    var getSnapshotter = DispatchSemaphore(value: 25)
    
    processQueue.async
            {
                var centerpoint = CLLocationCoordinate2D()
                centerpoint.latitude = (topRight.latitude + bottomLeft.latitude) / 2.0
                centerpoint.longitude = (topRight.longitude + bottomLeft.longitude) / 2.0
                let latitudeDelta = abs(topRight.latitude - bottomLeft.latitude)
                let longitudeDelta = abs(topRight.longitude - bottomLeft.longitude)
                let mapSpan = MKCoordinateSpanMake(latitudeDelta, longitudeDelta)
    
                var mapRegion = MKCoordinateRegion()
                mapRegion.center = centerpoint
                mapRegion.span = mapSpan
    
                let options = MKMapSnapshotOptions()
                options.region = mapRegion
                options.mapType = .standard               
                options.scale = 1.0
                options.size = CGSize(width: 256, height: 256)
    
                myself.getSnapshotter.wait()       // Limit the number of concurrent snapshotters since we could invoke very many
    
                let snapshotter = MKMapSnapshotter(options: options)
    
                snapshotter.start(with: myself.snapshotQueue, completionHandler: {snapshot, error in
                    if error == nil
                    {
                        self.saveTile(path: path, tile: snapshot!.image, z: z, x: x, y: y)
                        // saveTile writes the image out to a file in the mapOverlay file scheme
                    } else {
                        print("Error Creating Map Tile: ", error!)
                    }
                    if myself.getSnapshotter.signal() == 0
                    {  
                        // show status as completed (though could be up to 20 snapshots finishing, won't take long at this point 
                    }
                })
        }
    

    This works for me in getting up to 5K snapshots to build a 7-zoom level offline map image set without blocking the UI, so I'm pretty comfortable with the code.