:  ~ 8 min read

Extracting the location from a photo

I’d like to quickly explain how to let the user pick a photo and automatically extract the location for them. The post is targeted at iOS 11+, because starting with this version, to use an UIImagePickerController we don’t need to ask the user’s permission to access their photo library, because the controller runs as a separate process from the app, which means the app gets read-only access only to the image the user selected, and just to the image — no metadata included.

For this, we will need the UIImagePickerController:

func pickPhotoFromLibrary() {
   // 1
   guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { return }

   let imagePicker = UIImagePickerController()
   imagePicker.delegate = self // 2
   imagePicker.sourceType = .photoLibrary // 3
   imagePicker.allowsEditing = false // 4

   present(imagePicker, animated: true, completion: nil)
}

First, we need to make sure the type of source we want is available (1), in our case .photoLibrary (2). We’ll then set a delegate where the picker will give us the picked image, set the sourceType (3) and set if the user is allowed to edit the image, or not (4).

Next, the bread and butter:

// 1
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
   defer { dismiss(animated: true, completion: nil) } // 2
   guard let image = info[UIImagePickerControllerOriginalImage] as? UIImage else { return } // 3

   if let asset = info[UIImagePickerControllerPHAsset] as? PHAsset, // 4
      let location = asset.location { // 5
      CLGeocoder().reverseGeocodeLocation(location) { placemarks, error in // 6
         guard let placemark = placemarks?.first else { return } // 7

         /*
         Do something with
         placemark.name
         placemark.locality
         placemark.country ...
         */
      }
   }

   // Do something with the image.
}

This is the method the picker will call after the user has selected his image (1). First thing we do is defer the dismissal of the picker, since the system doesn’t do that by itself (2). Next, we try to extract the image from the info dictionary we have received (3).

Now, to extract the location, an UIImage is not enough, we need a PHAsset. To receive a PHAsset in the info dictionary, we do need access to the user’s library, which they may decline, so that’s why we’re using an if let here, instead of trying to extract the asset (4) and its location (5) in the guard above (at 3).

After we have the CLLocation (5), we need a CLGeocoder that can reverse-geocode the location via an async request (6), which will return an array of CLPlacemarks. Most of the time, this array will contain only one element, unless the address couldn’t be resolved to a single location, which is why we mostly care about the first element in the array (7).

There’s a final catch: as we saw above, to actually receive a PHAsset, we need access to the user’s library, so we need to ask for it first. In order to be allowed to ask this, we need to add a message in our Info.plist under the NSPhotoLibraryUsageDescription key.

Let’s update our pickPhotoFromLibrary method to do that:

func pickPhotoFromLibrary() {
   if PHPhotoLibrary.authorizationStatus() == .notDetermined { // 1
      DispatchQueue.main.async { // 2
         PHPhotoLibrary.requestAuthorization { _ in // 3
            DispatchQueue.main.async { // 4
               self.pickPhotoFromLibrary() // 5
            }
         }
      }

      return
   }

   guard UIImagePickerController.isSourceTypeAvailable(.photoLibrary) else { return }

   let imagePicker = UIImagePickerController()
   imagePicker.delegate = self
   imagePicker.sourceType = .photoLibrary
   imagePicker.allowsEditing = false

   present(imagePicker, animated: true, completion: nil)
}

We first check the current status and if it’s .notDetermined — which means we haven’t asked yet — we ask for authorisation (3). Since this will present an alert, we can be extra safe and do it on the main thread (2).

At this point, the alert asking for access to their library will be show to the user. Since the user can still pick photos even if they decline, we need an explicit message of what’s happening, otherwise the user might get confused, or worse, panic. I went for something along the lines of: ”To automatically determine a photo's location, we need access to your library. You can still pick photos even if you don't allow, but you'll have to manually enter locations.”

After the user taps Allow or Don’t allow, the request’s handler is called, at which point we’ll call pickPhotoFromLibrary again (5), also on the main thread (4). At this point, the authorizationStatus will be determined (restricted, denied or authorized) and the UIImagePickerController will be presented.

There are a few extra steps for offering this small feature, but I think it’s worth it if you need the location, as it brings a nice touch; for us it was.