How do you geofence 500 stores when an app may monitor only 20 regions at a time?

17.9.2 · L5 · Interview

Predict first

What changes between refreshes?

import Foundation
import CoreLocation

struct Store { let id: Int; let loc: CLLocation }
var seed: UInt64 = 42
func rnd() -> Double { seed = seed &* 6364136223846793005 &+ 1442695040888963407; return Double(seed >> 33) / Double(1 << 31) }
let stores = (0..<500).map { Store(id: $0, loc: CLLocation(latitude: 18.3 + rnd() * 0.5, longitude: 73.6 + rnd() * 0.5)) }

func nearest(_ n: Int, to p: CLLocation) -> [Store] { Array(stores.sorted { $0.loc.distance(from: p) < $1.loc.distance(from: p) }.prefix(n)) }
var monitored = Set<Int>()
func refresh(at p: CLLocation) {
    let want = Set(nearest(20, to: p).map(\.id))
    let stop = monitored.subtracting(want), start = want.subtracting(monitored)
    print("monitoring \(want.count): stop \(stop.count), start \(start.count)")
    monitored = want
}
let a = CLLocation(latitude: 18.52, longitude: 73.85)
refresh(at: a)
refresh(at: a)                                              // nothing moved
refresh(at: CLLocation(latitude: 18.53, longitude: 73.86))   // moved about 1.5 km
let far = nearest(20, to: a).last!
print("20th nearest store is", Int(far.loc.distance(from: a)), "m away")
  • 20 start, then nothing, then 2 stop and 2 start
  • 20 start every time
  • Nothing starts the first time

Mental model. A dockworker with 20 hooks hangs the 20 coats nearest the door and swaps them as the crowd moves.

Monitor a sliding window: keep the 20 nearest candidates registered, and when the user moves (a significant-change wake-up, or a boundary crossing of a larger “refresh” region) recompute and apply only the difference with stopMonitoring and startMonitoring. The run does this over 500 generated stores: the first call starts 20, an identical call changes nothing, and after moving about 1.5 km two regions stop and two start. Sorting 500 items by distance is cheap; for tens of thousands use a spatial index or server-side tiling. The 20th-nearest store is 6312 m away here, which shows why a fixed small radius around each store, not a huge one, is what you register: the nearest-20 set decides which stores, the radius decides how precisely.

Iterate on reality: entry and exit events are delayed and fuzzy (the system relies on Wi-Fi and cellular data, and small radii under roughly 100 to 200 m are unreliable), the app is relaunched in the background for events, and the set must be rebuilt after relaunch. iOS 17 also offers CLMonitor with conditions and an async event stream, with the same limit idea. The selection logic ran here with synthetic stores; real region events and the exact limit were not run in this check.

Only the difference between the old and new nearest-20 sets is applied.

Common traps

  • Registering everything. The system caps regions; you silently lose some.
  • Re-registering all 20 on every move. Apply only the difference.

They might follow up with

  • What triggers the recompute? Significant-change updates or a coarse outer region.

Related questions

  • 17.2.1 How does Core Location work: authorization levels, accuracy, significant-change and visits, and geofencing?
  • 17.9.1 How do you filter a stream of location fixes: accuracy, age, and implausible jumps?

← How do you filter a stream of location fixes: accuracy, age, and…   How does MapKit’s projected plane behave: map points, metres at… →