Location services in iOS: a hands-on guide for engineers
Location data is one of the most powerful—and most sensitive—features you can integrate into an iOS app. Users trust you with their whereabouts, and that trust is fragile. In this guide, we'll walk through the practical patterns that keep your app secure, efficient, and compliant while delivering real value to your users.
Understanding the authorization landscape
Apple's location privacy model has evolved significantly over the years. Before you request any location data, you need to understand the three authorization states: denied, restricted (device-wide parental control), and authorized. Within authorized, there are two granular options: When In Use and Always. Most engineers new to location services skip straight to requesting Always permission, which is a mistake.
The system itself discourages Always authorization. Users see it as intrusive, and Apple's App Review team scrutinizes apps that request it without compelling justification. When In Use permission is the right starting point for the vast majority of use cases: ride-hailing apps delivering a driver to your location, mapping apps showing your position on a route, or fitness trackers logging your run.
Set up your Info.plist correctly first. You need two keys:
NSLocationWhenInUseUsageDescription
NSLocationAlwaysAndWhenInUseUsageDescriptionThe description strings should be clear and specific. "We need your location to show nearby restaurants" is better than "We need your location." Users read these, and vague language triggers denials.
When-in-use authorization first
Request When In Use permission before anything else. This is your default starting point. The permission dialog appears only once per app, so you get one chance to make a good impression.
Here's the pattern we use across most of our services:
import CoreLocation
class LocationManager: NSObject, CLLocationManagerDelegate {
private let manager = CLLocationManager()
var authorizationStatus: CLAuthorizationStatus {
manager.authorizationStatus
}
override init() {
super.init()
manager.delegate = self
}
func requestWhenInUseAuthorization() {
// Check current status first
switch manager.authorizationStatus {
case .notDetermined:
manager.requestWhenInUseAuthorization()
case .restricted, .denied:
// Guide user to Settings
break
case .authorizedWhenInUse, .authorizedAlways:
// Already authorized
break
@unknown default:
break
}
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
// Handle authorization changes
// This callback fires when user changes permissions in Settings
}
}Notice we check the current status before requesting. If permission is already granted or denied, don't re-request. The system ignores duplicate requests anyway, but it's cleaner to be explicit.
When users deny permission, don't nag them immediately. Instead, show contextual UI that explains why you need location. If they tap that UI and we reach it again later, then request. This two-step approach respects user intent and improves grant rates.
CLLocationUpdate async API
iOS 17 introduced CLLocationUpdate, a modern async/await interface for location updates. If you're targeting iOS 17+, use it instead of the older delegate pattern. It's cleaner, safer from a memory perspective, and integrates naturally with Swift's concurrency model.
Here's how it works in practice:
import CoreLocation
class LocationTracker {
private let manager = CLLocationManager()
func startTrackingLocation() {
Task {
do {
// Request authorization first
let status = CLLocationManager.authorizationStatus()
if status == .notDetermined {
manager.requestWhenInUseAuthorization()
// Wait a moment for system to update status
try await Task.sleep(nanoseconds: 500_000_000)
}
// Stream location updates
for try await update in manager.liveLocationUpdates() {
let location = update.location
print("Latitude: \(location.coordinate.latitude)")
print("Accuracy: \(location.horizontalAccuracy)")
}
} catch {
print("Location error: \(error)")
}
}
}
}The liveLocationUpdates() method returns an async sequence of CLLocationUpdate objects. Each update includes the location, timestamp, and accuracy information. The sequence automatically stops when you cancel the task or exit the scope.
If you need to support iOS 16 or earlier, stick with the delegate pattern, but structure it with modern error handling and clear state management. Avoid global location managers; tie the manager's lifecycle to the screen or feature that uses it.
Reduce accuracy when possible
Location accuracy is a spectrum. Do you really need precision to 5 meters, or would 100 meters suffice? Users' battery drains faster when you request high precision, and your app may appear in their privacy dashboard more frequently.
Set the accuracy level explicitly:
manager.desiredAccuracy = kCLLocationAccuracyHundredMetersCommon accuracy levels:
kCLLocationAccuracyBest(~5m) – Only for real-time navigation or precise coordinate capturekCLLocationAccuracyNearestTenMeters(~10m) – For ride-hailing pickup locationskCLLocationAccuracyHundredMeters(~100m) – For "show me nearby stores" featureskCLLocationAccuracyKilometer(~1000m) – For city-level weather or broad analytics
There's also reduced accuracy mode, available since iOS 14. When a user grants permission but enables "Approximate Location," you receive a location radius of roughly 5 km. Handle this gracefully:
if location.horizontalAccuracy > 5000 {
// This is approximate location
// Skip features that require precision
} else {
// Use location normally
}We've seen teams reject approximate locations entirely, which frustrates users who deliberately chose that option to protect privacy. Instead, adapt your feature set. Show broader results, use less precise matching, or defer to server-side coarse geolocation.
Explain usage clearly
The permission prompt is your contract with the user. Make it count. The description string must explain why you need location and when you use it.
Bad: "Location needed for app functionality"
Good: "We use your location to show nearby restaurants and estimate delivery times while you're ordering."
If you need Always authorization, your explanation must be even stronger. We only use Always in apps where continuous background location tracking is genuinely necessary—geofencing for delivery zones, running route tracking with the app backgrounded, or emergency services dispatch.
Beyond the initial prompt, keep location transparent throughout the app. Show users when location is active. In iOS 17+, the system shows a blue banner at the top when an app accesses location, but don't rely on that alone. Offer toggles, settings, and clear feedback.
For analytics, be cautious. Some teams log every location update for behavioral analysis, which is unnecessary and erodes trust. If you collect location data for analytics or improvement, anonymize it, aggregate it, and store it server-side only—never log raw coordinates on device.
Common mistakes
Requesting Always permission on launch. This is the quickest way to low App Store ratings. Always is for background location only. If your app needs location only while in use, request When In Use.
Ignoring accuracy settings. We've debugged cases where apps drain battery requesting best accuracy when hundred meters would work fine. Profile your feature's true accuracy needs and set accordingly.
Not handling denial gracefully. Users deny location for many reasons. Some are permanent, some temporary. Show a message like "Location helps us serve you better, but you can enable it anytime in Settings" and move on. Don't block the entire app.
Caching stale locations indefinitely. Location data ages quickly. A cached location from 10 minutes ago may be miles away if the user moved. Establish a reasonable TTL (usually 2–5 minutes) and refresh on demand.
Starting location updates without stopping them. If you call the equivalent of startUpdatingLocation() in viewDidLoad and never stop it, location continues draining battery even when the app is backgrounded or the feature is unused. Use the async/await pattern where tasks naturally stop when deallocated, or explicitly call stopUpdatingLocation() in corresponding cleanup code.
Key takeaways
- Request When In Use authorization by default; Always is a special case with high user skepticism and App Review scrutiny.
- Use the modern
CLLocationUpdateasync API on iOS 17+ for cleaner, safer code that integrates with Swift concurrency. - Set accuracy explicitly to match your feature's actual requirements, reducing battery drain and respecting user privacy.
- Write clear, specific descriptions for location permissions so users understand exactly why you need their data.
- Handle denied and approximate locations gracefully; adapt features rather than blocking the app entirely.
- Never cache location indefinitely; refresh on demand and establish a reasonable TTL.
- Stop location updates when they're no longer needed to prevent unnecessary battery drain.






Comments
Loading comments…