●PUBLISH — Rork Max handles code signing and provisioning so you can submit to the App Store in two clicks●SIMULATOR — A browser streaming simulator feels close to real hardware, and a QR code installs to your device without TestFlight●MAX — Rork Max generates native Swift apps, powered by Claude Code and Claude Opus 4.6●NATIVE — Rork Max reaches Xcode-class territory: AR/LiDAR, Metal 3D, widgets, Live Activities, HealthKit, and Core ML●FUNDING — Rork raised $15M to power the next generation of App Store entrepreneurs●PRICE — Plans start free, paid tiers from $25/month, and Rork Max at $200/month●PUBLISH — Rork Max handles code signing and provisioning so you can submit to the App Store in two clicks●SIMULATOR — A browser streaming simulator feels close to real hardware, and a QR code installs to your device without TestFlight●MAX — Rork Max generates native Swift apps, powered by Claude Code and Claude Opus 4.6●NATIVE — Rork Max reaches Xcode-class territory: AR/LiDAR, Metal 3D, widgets, Live Activities, HealthKit, and Core ML●FUNDING — Rork raised $15M to power the next generation of App Store entrepreneurs●PRICE — Plans start free, paid tiers from $25/month, and Rork Max at $200/month
Working Around Rork Max's 20-Geofence Wall with Dynamic Re-registration
In a native Swift app generated by Rork Max, geofences you registered quietly stop firing past a certain count — and it's almost always iOS's silent limit of 20 monitored regions per app. Here's a dynamic re-registration design that keeps only the nearest 20 live, plus a Swift implementation you can drop in.
I was adding a small location-aware feature to one of my indie apps: when you arrive at a place, show a quiet image chosen for that place. Nothing more. To power it, I registered about 30 spots across the country as geofences.
On my own iPhone, the handful of spots near me fired correctly. But the spots later in the list stayed silent even when I physically walked to them. No error in the logs. The CLLocationManager code Rork Max generated looked perfectly intact.
The cause wasn't the code — it was an iOS ceiling. Region monitoring can watch at most 20 regions per app. Anything past the 20th is ignored with no exception and no warning. This article covers a design built around that ceiling — keeping only the nearest 20 registered at any moment — and the implementation you add on top of what Rork Max gives you.
Why the 21st Region Goes Quiet
CLLocationManager region monitoring (startMonitoring(for:)) carries a limit Apple states plainly: a single app can monitor at most 20 regions. Try to register past that, and startMonitoring(for:) neither crashes nor returns false. The excess simply never joins monitoredRegions, and didEnterRegion never fires for it.
So the symptom looks like this:
Registration order
Joins monitoredRegions?
didEnterRegion fires?
1st–20th
Yes
Yes
21st onward
No (silently)
No
Tell Rork Max "I want geofence notifications for 10 places," and it writes a straightforward for loop passing each one to startMonitoring(for:). At 10, nothing breaks. Only as your registration count grows — without anyone flagging the ceiling — does this surface as a low-reproducibility bug where "only the later ones don't respond."
For me, the part that ate the most time was that nothing errors. Core Location doesn't treat exceeding the limit as a failure, so even with print statements you only ever see "registered" logs. The first thing to check is manager.monitoredRegions.count right after registration. If it caps at 20 instead of climbing past it, the cause is confirmed as the limit, not a bug.
for region in regions { manager.startMonitoring(for: region)}// Always reconcile what you asked for against what's actually monitoredprint("requested: \(regions.count) / actually monitoring: \(manager.monitoredRegions.count)")
The moment you see requested: 30 / actually monitoring: 20, the question changes from "why won't it fire" to "which 20 should I pick."
Don't Remove the Limit — Rotate Through It
You can't raise the 20-region ceiling from app configuration. Since there's no way to grow it, you change the design instead: hold as many logical geofences as you like in your own store, and hand CLLocationManager only the 20 nearest to the user at any moment. When the user moves, swap the lineup. This dynamic re-registration is the heart of it.
The question is when to swap. That's where startMonitoringSignificantLocationChanges() comes in. It's a separate mechanism from region monitoring, delivering location updates at a coarse grain — roughly every 500 meters, no more than once every few minutes. The coarseness keeps power draw low, and the OS wakes your app to deliver updates even after it has been terminated. That's far too coarse for the arrival test itself (radii around 100m), but it's exactly the right grain for deciding when to rotate the 20.
Laid out, the roles are:
Role
API
Grain / traits
Actual arrival detection
region monitoring
~100m radius, max 20
Trigger to rotate the 20
significant location change
~500m, low power, relaunches after termination
Holding all geofences
your own store (array or DB)
unlimited count
It's a two-tier arrangement: reserve high-precision monitoring for a small set, and delegate the swapping to a coarse, power-frugal monitor. Even with hundreds of spots nationwide, only the nearest 20 are ever loaded into CLLocationManager.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You'll understand why the 21st geofence is silently ignored, and be able to tell that the symptom is an iOS design limit rather than a bug in your code
✦You get a dynamic re-registration manager that always keeps the 20 targets nearest the user live, shaped to drop into Rork Max's generated code
✦You'll know exactly where to fill in the parts that only surface on a real device — significant location changes, didDetermineState, and Always authorization
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Here's a thin wrapper you can add to Rork Max's generated code. It holds all candidate geofences (an array of GeoTarget) and re-registers only the 20 nearest the current location.
import CoreLocationstruct GeoTarget { let id: String let coordinate: CLLocationCoordinate2D let radius: CLLocationDistance // 100–150m recommended (see below)}final class GeofenceCoordinator: NSObject, CLLocationManagerDelegate { private let manager = CLLocationManager() private var allTargets: [GeoTarget] = [] // The iOS limit is 20. Hold to 18 as a safety margin (reason below). private let maxActiveRegions = 18 var onEnter: ((String) -> Void)? override init() { super.init() manager.delegate = self manager.allowsBackgroundLocationUpdates = true } func start(with targets: [GeoTarget]) { allTargets = targets manager.requestAlwaysAuthorization() // Coarse monitor that drives the rotation of the 20 manager.startMonitoringSignificantLocationChanges() if let here = manager.location { reregisterNearest(to: here.coordinate) } } /// Re-monitor only the maxActiveRegions nearest the current location private func reregisterNearest(to center: CLLocationCoordinate2D) { let origin = CLLocation(latitude: center.latitude, longitude: center.longitude) let nearest = allTargets .sorted { let a = CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude) let b = CLLocation(latitude: $1.coordinate.latitude, longitude: $1.coordinate.longitude) return a.distance(from: origin) < b.distance(from: origin) } .prefix(maxActiveRegions) let nextIDs = Set(nearest.map(\.id)) // Stop only those currently monitored that aren't in the new set of 20 for region in manager.monitoredRegions where !nextIDs.contains(region.identifier) { manager.stopMonitoring(for: region) } // Start only those newly entering the set let currentIDs = Set(manager.monitoredRegions.map(\.identifier)) for target in nearest where !currentIDs.contains(target.id) { let region = CLCircularRegion( center: target.coordinate, radius: target.radius, identifier: target.id ) region.notifyOnEntry = true region.notifyOnExit = false manager.startMonitoring(for: region) } } func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { guard let here = locations.last else { return } reregisterNearest(to: here.coordinate) } func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) { onEnter?(region.identifier) }}
Two choices here are deliberate. First, instead of tearing everything down and re-registering every time, it updates only the diff. stopMonitoring and startMonitoring are themselves registration operations with the system, so wholesale swaps thrash when the user walks back and forth near a boundary. Leaving untouched the targets that survive into the new 20 damps that churn.
Second, it holds to 18 rather than the full 20. The region-monitoring budget can be consumed not just by your code but by other features or some libraries in your app. Exhaust it, and the last couple you registered get quietly ignored again. Leaving a few slots of headroom keeps you out of that contention.
The Pitfalls That Only Surface on a Real Device
Simulating a location in the simulator won't faithfully reproduce region monitoring. This feature assumes a real device. Rork Max gives you both the in-browser streaming simulator and a path to install directly onto your own iPhone via QR, so the reliable move is to install through the latter and walk it. Here are the real-device pitfalls, roughly in the order you'll hit them.
If you're already inside a region, didEnterRegion won't fire. The arrival event triggers the instant you cross the boundary from outside to inside. If you're standing on the spot when the app launches, no crossing happens, so no notification arrives. Right after launch or re-registration, call manager.requestState(for:) and, when didDetermineState reports .inside, treat it as an arrival yourself.
func locationManager(_ manager: CLLocationManager, didDetermineState state: CLRegionState, for region: CLRegion) { if state == .inside { onEnter?(region.identifier) // cover the case of being inside without a crossing }}
Don't make the radius too small.CLCircularRegion has an effective lower bound on radius tied to the device and hardware; specify something well under 100m and it won't actually detect at that precision. The urge to trigger pinpoint-tight is understandable, but staying around 100–150m makes arrivals far less likely to be missed.
You need Always authorization. To catch arrivals while the app is closed, "Always" permission is required. Asking for Always cold tends to get denied, so the realistic path is to earn When In Use first and prompt for the upgrade to Always in the context where the feature is used. Alongside that, add NSLocationAlwaysAndWhenInUseUsageDescription and NSLocationWhenInUseUsageDescription to Info.plist, and Location updates under Background Modes in Signing & Capabilities. This is territory Rork Max's generated code can't fill in, so you supply it by hand. If location itself won't budge, my walkthrough on fixing location permission that won't take effect lays out the isolation steps and gives you the full picture of where things stall.
Since iOS 17, CLMonitor is available as the successor to region monitoring. You work with CLMonitoringCondition through an async flow, and the monitoring state is structured, so it reads more clearly than the old delegate-based approach. If you're writing arrival detection fresh, it's worth considering.
But moving to CLMonitor doesn't retire the "narrow to the nearest and rotate" design. The number of conditions you can watch still has a practical ceiling — you can't load hundreds of geofences at once. Even with a newer API, the idea of re-selecting a small live set from many candidates stays the same. The structure this manager builds — hold all candidates yourself, load only the near ones into the OS — ports straight onto CLMonitor. My rule of thumb: migrate once you can require iOS 17+, and stay on region monitoring while you still support older OSes.
The Next Step
Start by adding a single line that prints manager.monitoredRegions.count right after your current geofence registration. If it's stuck at 20, it's worth swapping in this article's manager. If it comfortably sits under 20, you don't need dynamic re-registration yet — resisting the urge to complicate things ahead of need is, I think, an important call in indie development too.
Location is a domain where much of the truth only reveals itself once you're walking with a real device. I still get surprised by per-device behavior differences, but once you fold the limit into your design as a premise rather than a bug, it becomes a great deal easier to work with. Thank you for reading.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.