skip to Main Content

On MacOS I was able to get the place names from all my 90,000 of photos by querying the Photos.sqlite database.

I’m trying to do something similar in iOS now, but have only found coordinate.latitude and coordinate.longitude.

I see there’s a reverseGeocodeLocation that takes a coordinate and returns a CLPlacemark, but geocoding requests are rate-limited for each app.

If I look in Photos app on the iphone I can see my pics have place name metadata already, is it exposed in an API somewhere?

private func fetchPhotoLocations() {
    let fetchOptions = PHFetchOptions()
    fetchOptions.includeHiddenAssets = false
    fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
    
    let assets = PHAsset.fetchAssets(with: fetchOptions)
    
    assets.enumerateObjects { (asset, index, stop) in
        if let location = asset.location {
            let resources = PHAssetResource.assetResources(for: asset)
            let filename = resources.first?.originalFilename ?? "Unknown"
            
            let options = PHAssetResourceRequestOptions()
            options.isNetworkAccessAllowed = false
            
            if let resource = resources.first {
                PHAssetResourceManager.default().requestData(for: resource, options: options) { (data) in
                    
                } completionHandler: { (error) in
                    if let error = error {
                        print("Error loading resource: (error)")
                    }
                }
                
                DispatchQueue.main.async {
                    self.photoLocations.append(PhotoLocation(
                        id: asset.localIdentifier,
                        coordinate: location.coordinate,
                        creationDate: asset.creationDate ?? Date(),
                        filename: filename
                    ))
                }
            }
        }
    }
}

2

Answers


    • Instead of querying for every single photo, you can batch process locations and avoid duplicates by caching geocoding results.

    • Use CLGeocoder efficiently by avoiding excessive simultaneous requests.

    • Unfortunately, the detailed place name metadata (like those displayed in the Photos app) is not publicly available via the Photos framework.

    Here’s the corrected version of your code:

    import Photos
    import CoreLocation
    
    struct PhotoLocation {
        let id: String
        let coordinate: CLLocationCoordinate2D
        let creationDate: Date
        let filename: String
        let placeName: String? // Optional for reverse geocoded place names
    }
    
    class PhotoManager {
        private let geocoder = CLGeocoder()
        private var geocodeCache = [CLLocationCoordinate2D: String]() // Cache for geocoded locations
        private(set) var photoLocations: [PhotoLocation] = []
    
    func fetchPhotoLocations() {
        let fetchOptions = PHFetchOptions()
        fetchOptions.includeHiddenAssets = false
        fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
        
        let assets = PHAsset.fetchAssets(with: fetchOptions)
        
        assets.enumerateObjects { [weak self] (asset, index, stop) in
            guard let self = self else { return }
            
            if let location = asset.location {
                let resources = PHAssetResource.assetResources(for: asset)
                let filename = resources.first?.originalFilename ?? "Unknown"
                
                let coordinate = location.coordinate
                
                // Check cache before making a geocoding request
                if let cachedPlaceName = self.geocodeCache[coordinate] {
                    self.addPhotoLocation(asset: asset, coordinate: coordinate, filename: filename, placeName: cachedPlaceName)
                } else {
                    // Perform reverse geocoding
                    self.geocoder.reverseGeocodeLocation(location) { [weak self] (placemarks, error) in
                        guard let self = self else { return }
                        
                        let placeName = placemarks?.first?.locality ?? placemarks?.first?.name
                        if let placeName = placeName {
                            self.geocodeCache[coordinate] = placeName // Cache the result
                        }
                        
                        self.addPhotoLocation(asset: asset, coordinate: coordinate, filename: filename, placeName: placeName)
                    }
                }
            }
        }
    }
    
    private func addPhotoLocation(asset: PHAsset, coordinate: CLLocationCoordinate2D, filename: String, placeName: String?) {
        DispatchQueue.main.async {
            let photoLocation = PhotoLocation(
                id: asset.localIdentifier,
                coordinate: coordinate,
                creationDate: asset.creationDate ?? Date(),
                filename: filename,
                placeName: placeName
            )
            self.photoLocations.append(photoLocation)
        }
    }
    

    For large photo libraries, you can:

    • Limit the number of photos processed at a time.
    • Add pagination to batch process metadata.
    Login or Signup to reply.
  1. You can greatly optimize your reverse geocoding by taking advantage of some things:

    1. We tend to shoot a lot of photos in the same general location. (Around the neighborhood. At the park. At the kids’ school. On a vacation in some other city.)
    2. The regions that are assigned the same city/municipality name are big. Really big.
    3. For a given latitude, the number of kilometers per degree of latitude and longitude don’t change very much unless you move to a very different latitude.*

    Here’s what I would do: Sort your pictures to be reverse-geocoded by the date they were taken. (That should be in the EXIF metadata. You want to process them in the order they were taken because we tend to take a whole series of shots in one general location.) Take your first picture. Fetch the lat/long from the EXIF data. Calculate kilometers/degree of latitude and kilometers/degree of longitude. Save those values.

    Create an empty table of city/state names and latitudes/logitudes.
    Reverse geocode that first picture. Save its city/state and lat/long into your table, and remember the index in the table. (If you want to improve your data, fetch and save the lat/long of the centroid of the city/municipality, and its approximate diameter if available. Then when matching new locations, you can see if the new location is roughly inside that diameter.)

    Load your next picture. Use your kilometers/degree values to calculate how far that picture’s location is from the current city/state you are working with. If it’s within some margin (say 1/2 kilometer) then just assume it’s in the same city/state, and assign that picture to the city/state without reverse-geocoding.

    If a new picture is too far away from the previous picture to assume it’s in the same city/state, do math on all the other saved city/states in your table to find the distance between the new picture and each of those city/states. If you find a city/state close enough, assign that one to the picture. If not, reverse-geocode your new picture’s location and save IT to your city/state table.

    Write code that runs the above process in the background, working through your photo library, tracking how many reverse-geocoding requests you make per minute, and simply pause when you hit the rate limit. (Note that on iOS, apps can’t really run in the background. You’d write your code to do its batch processing on a background thread so the UI stays responsive, and you’d need to keep the app frontmost and keep your phone awake in order for it to keep getting processor time. (If this is for personal use, you can cheat and do things like trigger a long-playing sound and set yourself up as a music playing app. Those are allowed to run in the background, and if you don’t submit it to the App Store you can get away with the cheat.)

    If you shoot a large batch of pictures within a day’s drive, the above grouping approach should enable you to get city/state data for thousands of pictures with just a handful of reverse-geocoding requests.


    *If you travel to wildly different latitudes, like Alaska and Key West, my suggestion of calculating a fixed number of kilometers per degree of latitude and longitude won’t work well because the distance between degree lines changes too much in different latitudes.

    If you want to make your calculations more accurate you can look up the "Havershine formula", which lets you calculate "great circle" distances between any 2 points on the globe. It uses 3D trig to calculate distances between points on the surface of a sphere. It’s much more math-intensive than the approximations I suggested, and it’s more work to figure out how to use it, but it will work anywhere on the globe, from the poles to the equator. On a modern computer/phone it’s still quite fast.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search