2

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.

TealShift
  • 842
  • 5
  • 19

1 Answers1

7

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.

Ron Diel
  • 1,354
  • 7
  • 10
  • As to the need to be in the foreground, that's correct so I use the command: [ UIApplication.shared.isIdleTimerDisabled = true ]. Just be sure to set it to false once you're done. – Ron Diel Nov 10 '16 at 22:09
  • Wow, looks impressive. I'm not real familiar with advanced queuing like this. May take me a little while to try some form of this solution. – TealShift Nov 12 '16 at 06:31
  • That's disappointing foreground is required since I'm pulling workout data from Apple Watch and the only thing stopping bg processing is this dumb function. :/ – TealShift Nov 12 '16 at 06:32
  • Only a handful of functions work in background - things like playing and recording sound, tracking location, doing VOIP calls, etc. There's more about this at https://developer.apple.com/library/content/documentation/iPhone/Conceptual/iPhoneOSProgrammingGuide/BackgroundExecution/BackgroundExecution.html – Ron Diel Nov 12 '16 at 18:55