Skip to content

Reverse geocoding should not occur multiple times per session #462

Open
@vdhamer

Description

@vdhamer

If the user scrolls up and down within the Clubs and Museums screen, any map that gets displayed triggers (.onAppear) an asynchronous call to reverseGeocode. This fetches localized Town and Country strings that match the current language setting of the device.

This use of onAppear means the app can send multiple identical requests to Apple's mapping server. Even once is a bit more than needed: the results are stored in Core Data. But recomputing .onAppear means that any club/museum coordinate updates or a change in the device's language setting are automatically handled just before the data is displayed.

Drawbacks of multiple calls to reverseGeocode

  • extra calls = extra bandwidth
  • town and country can disappear ("Town?") if the network connection is unreliable (why/how?)
  • the server's API is rate limited by Apple
            .onAppear {
                // on main queue (avoid accessing NSManagedObjects on background thread!)
                let clubName = filteredOrganization.fullName
                let town = filteredOrganization.town // unlocalized
                let coordinates = filteredOrganization.coordinates

                Task.detached { // other (non-bgContext) background thread to access 2 async functions
                    var localizedTown: String
                    var localizedCountry: String?
                    do {
                        let (locality, nation) = // can be (nil, nil) for Chinese location or Chinese user location
                            try await reverseGeocode(coordinates: coordinates)
                        localizedTown = locality ?? town // unlocalized as fallback for localized -> String
                        localizedCountry = nation // optional String
                        await updateTownCountry(clubName: clubName, town: town,
                                                localizedTown: localizedTown, localizedCountry: localizedCountry)
                    } catch {
                        print("ERROR: could not reverseGeocode (\(coordinates.latitude), \(coordinates.longitude))")
                    }
                }
            }

Alternative solutions

  1. Store (in volatile static dictionary?) the list of clubs that have already been reverseGeocoded in this session.
    ⇒ API usage is one call per club per session.
    Verdict: not enough benefit for the effort?

  2. Store (Core Data) the date when localizedTown and localizedCountry were last refreshed. Until date+N days, we assume any available translations are stil valid, and an API call is saved.
    Needs special handling at startup if the current locale matches the previous locale (UserData). If there is no match, all translations stored in Core Data are invalidated (by changing the date or setting the strings to nil).
    Needs special handling (in the unlikely case) that the coordinates of an existing club are changed: also invalidate that record.
    The locale/coordinate guarding should handle normal updating; the date trick is just to handle sever-side data changes.
    Multiple clubs can get invalidated on the same day if they were first loaded/viewed on the same day. This can be handled by spreading the load across a few days (Int.random(0...10))
    ⇒ API usage is one call per club per N days. Say N=100 days.

  3. Same as 2, but without the date-based safety net.
    ⇒ API usage is one call per club (unless an update is needed, close to absolute minimum).
    Can serve as intermediate step to get to 2.

  4. Same as 2, but with with the strings stored in UserData instead of CoreData organization objects.
    Implement as a cache on the reverseGeocode(coordinates: coordinates, locale) request, without caring about the Organization being processed. Cache can be stored as a dictionary in UserDefaults. For 1000 organizations and a user who only uses a single language, this is a dictionary with 1000 structs with 5 fields: language, latitude, longitude, localizedTown, localizedCountry.
    If needed, nearby locations (say 250m) can result in cache hits. This will occasionally save API calls if there are multiple organizations in the same building, or if a coordinate shifts slightly (e.g. extra digit of precision added).
    ⇒ API usage is one call per club (unless an update is needed, close to absolute minimum).

  5. Store in CoreData entity the language(s) for which the localized Town/Country was computed. If new language appears, recompute. If coordinates change, force a recompute by setting the language to nil or a dummy language. So this requires only 1 stored field.
    ⇒ API usage is one call per club (unless an update is needed, close to absolute minimum). This is absolute minimum if multiple languages are "cached".

Steps to get to 2nd alternative

  • if localizedTown and localizedCountry are available, use them without a call to the reverseGeocoding API. Causes each club to be translated only once. And not to react to server-side, language or coordinate changes.
  • detect language change at startup and invalidate translations by setting all strings to nil (needs a bool because nil means "ignore")
  • detect a coordinate change and invalidate translations for that organization
  • add a date to force occasional refresh of the cache

Metadata

Metadata

Assignees

Labels

ConcurrencySwift structured concurrency & Swift6 data race preventionEnhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions