When I was building an app that passively tracks which venues users are at, I quickly realized the whole thing falls apart without "Always" location access. The background geofencing, the significant location changes that keep things running when the app is killed — both require Always. It was a hard requirement.
That meant thinking carefully about how to ask for it, what to do when users granted less than I needed, and how to handle every state the OS could put me in. Here's how I approached each part.
Why "Always" is a hard requirement
Apple's location permission model has two tiers. "When In Use" gives you location updates while the app is in the foreground or actively running in the background. "Always" gives you access to significant location change monitoring and region monitoring, both of which can wake or relaunch a killed app.
For an app that needs to detect venue entry and exit passively, "When In Use" stops working the moment the app closes.
Initializing the LocationManager early
The first thing I got wrong was initializing LocationManager inside a view. If iOS relaunches the app in the background due to a location event, it goes through AppDelegate first. Your LocationManager needs to be alive by then. Initialize it after views load and the event is gone before your code ever runs.
The fix is to initialize it in AppDelegate, before any views exist:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
_ = LocationManager.shared
return true
}
One line, but it determines whether background events get delivered at all.
The permission state machine
CLAuthorizationStatus has five states worth handling differently:
.notDeterminedUser hasn't been asked yet. CallrequestAlwaysAuthorization()and wait..authorizedWhenInUseUser granted partial access. The system prompt for "Always" can still be shown..authorizedAlwaysFull access, everything works..deniedUser explicitly denied. The only option is sending them to Settings..restrictedParental controls or MDM policy. No prompting is possible.
Most apps treat these as binary. The difference between .notDetermined and .denied matters: .notDetermined allows a system prompt; .denied sends the user to Settings.
The hard gate
I decided early to gate the main UI on Always access. The core feature requires it, and letting users in with partial access just creates confusion. The gate in ContentView:
if locationManager.authorizationStatus != .authorizedAlways {
LocationRequestView()
} else {
MainAppView()
}
This applies both during onboarding and after. If a user downgrades their permission in Settings and comes back to the app, they see the request screen again instead of a broken experience.
The WhenInUse to Always upgrade path
iOS supports upgrading from "When In Use" to "Always". Calling requestAlwaysAuthorization() after the user already granted "When In Use" triggers the upgrade prompt. I handle this in two places.
First, in LocationRequestView on appear, I call requestAlwaysAuthorization() if the status is .notDetermined or .authorizedWhenInUse. For first-time users this triggers the system prompt immediately. For users who already granted partial access, it shows the upgrade dialog.
.onAppear {
let status = LocationManager.shared.authorizationStatus
if status == .notDetermined || status == .authorizedWhenInUse {
LocationManager.shared.requestAlwaysAuthorization()
}
}
Second, I watch for status changes in ContentView. If the user grants "When In Use" through the system prompt, I immediately ask to upgrade:
.onChange(of: locationManager.authorizationStatus) { _, status in
if status == .authorizedWhenInUse {
locationManager.requestAlwaysAuthorization()
}
}
When the user has already denied
After a user denies "Always" or denies location entirely, iOS blocks any further system prompts. The only path is Settings.
LocationRequestView handles this with a step-by-step explanation and an "Open Settings" button:
Button("Open Settings") {
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
}
The instructions in the view walk through exactly what to tap: Settings, then Location, then Always. Users who actually want the feature working will follow these steps.
The Settings return problem
There's a subtle iOS behavior that caught me: when a user changes location permissions in Settings and comes back to the app, didChangeAuthorization sometimes skips entirely. Your published authorizationStatus stays stale and the UI stays on the request screen even though the user just granted Always.
The fix is to re-read the authorization status explicitly whenever the app becomes active:
.onChange(of: scenePhase) { _, newPhase in
if newPhase == .active {
locationManager.refreshAuthorizationStatus()
}
}
And in LocationManager:
func refreshAuthorizationStatus() {
authorizationStatus = locationManager.authorizationStatus
}
Skip this and users who grant permission in Settings come back to the request screen still showing. They assume it broke and stop trying.
The same problem on init
A related issue happens on launch. If you declare authorizationStatus as @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined, any view observing it will briefly see .notDetermined even if the user granted "Always" a month ago.
The fix is to read the real status synchronously in init before any observer sees the value:
override init() {
super.init()
locationManager.delegate = self
authorizationStatus = locationManager.authorizationStatus // read real value immediately
}
Any view depending on authorizationStatus renders correctly on the first pass instead of flashing the wrong state.
What the full flow looks like
- First launch.
LocationManageris initialized inAppDelegate.authorizationStatusis read synchronously and is.notDetermined. ContentViewshowsLocationRequestView. On appear,requestAlwaysAuthorization()is called and the system prompt appears.- User taps "Allow While Using." Status becomes
.authorizedWhenInUse.ContentViewdetects this and immediately callsrequestAlwaysAuthorization()again to trigger the upgrade prompt. - User grants "Always." Status becomes
.authorizedAlways.LocationRequestViewis dismissed and the main app appears. - Later, user downgrades to "While Using" in Settings. They come back to the app,
scenePhasechanges to.active,refreshAuthorizationStatus()is called, status updates to.authorizedWhenInUse, andLocationRequestViewis shown again.
Every state is handled and the user always knows what's going on.