13

I'm learning Swift 3 on my own, and my current learning project involves allowing the user to snap a photo and get a map snapshot with the current location pinned.

I've relied on this answer from Aug 2015 and this answer from Jun 2016 for guidance, but I still can't find the right path.

Right now, I can...

  1. Get the photo from the buffer
  2. Get a map snapshot

But I just can't place the pin. I know that my code is incomplete and ineffective -- so this is more than just a debugging question. Here is what I've been working with (as well as many variations based on the links above):

let snapShotter = MKMapSnapshotter(options: mapSnapshotOptions)

snapShotter.start() {

    snapshot, error in

        guard let snapshot = snapshot else {
            return
        }

        let image = snapshot.image
        let annotation = MKPointAnnotation()
        annotation.coordinate = needleLocation  // is a CLLocationCoordinate2D
        annotation.title = "My Title"

        let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "annotation")

        // I want to push the final image to a global variable
        // can't figure out how to define finalImage as the pinned map
        self.myMap = finalImage


        } // close snapShotter.start

I've been stuck for days now, so I certainly would appreciate any insights. Thanks!

Community
  • 1
  • 1
niblettes
  • 165
  • 1
  • 8
  • Rob, thanks. I followed the link, and watched the MapKit video that link links to. However, being new to this I cannot bridge the gap between 4 year old Objective-C and current Swift 3 syntax and APIs. In fact haven't been able to bridge the gap between the few Swift 2 examples available and Swift 3. So I'm still stuck. – niblettes Mar 13 '17 at 16:29

2 Answers2

34

To render a MKMapSnapshot with annotation views, you have to manually draw the snapshot's image and the annotation view's image on a new graphic context, and get a new image from that context. In Swift 3:

let rect = imageView.bounds

let snapshot = MKMapSnapshotter(options: options)
snapshot.start { snapshot, error in
    guard let snapshot = snapshot, error == nil else {
        print(error ?? "Unknown error")
        return
    }

    let image = UIGraphicsImageRenderer(size: options.size).image { _ in
        snapshot.image.draw(at: .zero)

        let pinView = MKPinAnnotationView(annotation: nil, reuseIdentifier: nil)
        let pinImage = pinView.image

        var point = snapshot.point(for: location.coordinate)

        if rect.contains(point) {
            point.x -= pinView.bounds.width / 2
            point.y -= pinView.bounds.height / 2
            point.x += pinView.centerOffset.x
            point.y += pinView.centerOffset.y
            pinImage?.draw(at: point)
        }
    }

    // do whatever you want with this image, e.g.

    DispatchQueue.main.async {
        imageView.image = image
    }
}

This was adapted from https://stackoverflow.com/a/18776723/1271826, which itself was adapted from WWDC 2013 video Putting MapKit in Perspective.

Here is an example of a snapshot with a .satelliteFlyover:

enter image description here

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Rob, that worked almost perfectly. 'if rect.contains' throws a "Use of unresolved identifier" error. I tried CGRect, but got a "Cannot invoke contains..." error on the CLLOcationCoordinate2D point type. I've been reading about Core Graphic and contains, but cannot resolve the error. I can remove the IF statement to resolve it, but then of course I loose the check. – niblettes Mar 14 '17 at 00:01
  • I defined `rect` as the `bounds` of the `UIImageView` that I had used when creating the `size` for `MKMapSnapshotOptions`. I have added that declaration to my code sample... – Rob Mar 14 '17 at 00:04
  • That is really helpful. Of course I still get an error. UIImageView inherits .bounds from UIView. However MKMapSnapshot inherits from NSObject which doesn't provide .bounds. So it seems my code might be doing something wrong. I will post it below. – niblettes Mar 14 '17 at 19:43
  • 1
    @Rob Thank you for a wonderful answer, it worked like charm! Awesome! – Sergey Grischyov Jul 05 '17 at 17:24
  • 1
    Thanks! Very usefull! – dimazava Jul 24 '18 at 12:58
0

I was getting an error from Rob's code that defines "rect" because my MKMapSnapshotter image is a UIImage. However .bounds is a property of UIImageView and not UIImage. So the rect definition throws an error.

Here is how I configured the options:

        let mapSnapshotOptions = MKMapSnapshotOptions()

        // Set the region of the map that is rendered.
        let needleLocation = CLLocationCoordinate2DMake(location.coordinate.latitude, location.coordinate.longitude)
        let region = MKCoordinateRegionMakeWithDistance(needleLocation, 1000, 1000)
        mapSnapshotOptions.region = region

        // Set the scale of the image. We'll just use the scale of the current device, which is 2x scale on Retina screens.
        mapSnapshotOptions.scale = UIScreen.main.scale

        // Set the size of the image output.
        mapSnapshotOptions.size = CGSize(width: 300, height: 300)

        // Show buildings and Points of Interest on the snapshot
        mapSnapshotOptions.showsBuildings = true
        mapSnapshotOptions.showsPointsOfInterest = false

So is there another way to access the .bounds property? Or have I created my snapshot incorrectly?

niblettes
  • 165
  • 1
  • 8
  • You don't need `bounds`. The goal is just to make sure that the `CGPoint` for your annotation view rests within the visible image. Because I sized my snapshot's image to fit the `UIImageView`, I used `imageView.bounds`. But if you're creating your `CGSize` using some fixed size, then use that, e.g. `let rect = CGRect(origin: .zero, size: mapSnapshotOptions.size)`. Frankly, if you've created your mapview explicitly to point at the annotation, then the `if rect.contains(...) { ... }` stuff isn't needed. It's only needed if you're not 100% sure that the annotation is visible. – Rob Mar 14 '17 at 19:50