energy-systems-and-sustainability
How to Use Location Services to Power Geo-fencing in Ios
Table of Contents
Introduction to Geo-fencing in iOS and How Directus Simplifies Management
Geo-fencing transforms mobile apps by enabling context-aware responses when a user enters or exits a virtual boundary. In iOS, Core Location provides the infrastructure to monitor circular regions, but managing these boundaries—especially across a fleet of devices—can become complex. Directus, an open-source headless CMS, offers a flexible backend to define, store, and serve geo-fence data to your iOS app. By combining iOS location services with Directus, you can create dynamic, scalable geo-fencing experiences that update without app store re-releases.
This guide walks through the complete process: from setting up location permissions and monitoring regions in iOS to designing a Directus collection that feeds geo-fence definitions to your app. You’ll learn how to fetch boundaries from a Directus API, convert them into CLCircularRegion objects, and react to region events—all while following production best practices.
Understanding Geo-fencing on iOS
A geo-fence is a virtual perimeter—typically a circle defined by a center coordinate and radius—triggering events when a device crosses its border. iOS monitors fences even when the app is in the background (with proper authorization). Common use cases include:
- Proximity marketing: Send a coupon when a customer enters a store.
- Automation: Turn on smart lights when you arrive home.
- Asset tracking: Alert if equipment leaves a designated area.
- Safety: Notify caregivers when a person exits a safe zone.
iOS supports around 20 simultaneously monitored regions per app, making efficient management critical. Hardcoding geo-fences in the app limits flexibility. Using Directus, you can store hundreds of boundaries in a database and push only relevant ones to the client based on user context or device location.
Setting Up Directus as Your Geo-fence Backend
Directus provides a RESTful API and GraphQL endpoint, plus an admin dashboard for non-technical teams to manage data. For geo-fencing, you’ll need a collection that holds each fence’s properties.
Creating a “geo_fences” Collection
Design your schema with these fields:
- id (Primary key, auto-increment or UUID)
- name (String) – Human-readable label, e.g., “Downtown Store”
- identifier (String, unique) – Used as the iOS region identifier (e.g., “store_123”)
- latitude (Float, decimal degrees)
- longitude (Float, decimal degrees)
- radius_meters (Integer) – Radius in meters for
CLCircularRegion - notify_on_entry (Boolean)
- notify_on_exit (Boolean)
- active (Boolean) – Allows toggling fences without deleting them
- metadata (JSON) – Additional payload (coupon code, message, etc.)
- date_created / date_updated (Timestamps)
Set read permissions for the public or authenticated role so your iOS app can fetch the data. Optionally, add a user_id field if fences are user-specific.
Securing the API Endpoint
Directus supports static tokens, JWT, or OAuth2. For a mobile app, a static API token (for public collections) or device-specific token is easiest. Enable CORS in Directus if needed during development. Your API URL pattern will be:
GET /items/geo_fences?filter[active][_eq]=true
Implementing Location Services in Your iOS App
Core Location is the framework behind geo-fencing. You must configure permissions and instantiate a CLLocationManager.
Requesting Authorization
In Info.plist, add the following keys with a brief explanation:
- NSLocationWhenInUseUsageDescription – Needed if the app uses location only while open.
- NSLocationAlwaysAndWhenInUseUsageDescription – Required for background geo-fence monitoring.
For geo-fencing, request “Always” authorization. Example Swift code:
let locationManager = CLLocationManager()
locationManager.delegate = self
locationManager.requestAlwaysAuthorization()
Handle the delegate callback to know when authorization changes. Respect user denial by disabling geo-fencing features gracefully.
Checking Location Services Availability
Always verify that location services are enabled globally and for your app using CLLocationManager.locationServicesEnabled() and CLLocationManager.authorizationStatus(). Provide fallback UI if geo-fencing is unavailable.
Fetching Geo-fence Definitions from Directus
Your iOS app must retrieve fences from Directus each time it starts or periodically. Use URLSession to call the API and decode JSON into model objects.
Creating a Swift Model
struct GeoFence: Decodable {
let id: Int
let name: String
let identifier: String
let latitude: Double
let longitude: Double
let radiusMeters: Double
let notifyOnEntry: Bool
let notifyOnExit: Bool
let active: Bool
let metadata: [String: Any]?
enum CodingKeys: String, CodingKey {
case id, name, identifier
case latitude, longitude
case radiusMeters = "radius_meters"
case notifyOnEntry = "notify_on_entry"
case notifyOnExit = "notify_on_exit"
case active, metadata
}
}
Performing the API Request
func fetchGeoFences(completion: @escaping ([GeoFence]?) -> Void) {
guard let url = URL(string: "https://your-directus.com/items/geo_fences?filter[active][_eq]=true") else {
completion(nil)
return
}
var request = URLRequest(url: url)
request.setValue("Bearer YOUR_ACCESS_TOKEN", forHTTPHeaderField: "Authorization")
URLSession.shared.dataTask(with: request) { data, response, error in
guard let data = data, error == nil else {
completion(nil)
return
}
let decoder = JSONDecoder()
if let result = try? decoder.decode(DirectusResponse<[GeoFence]>.self, from: data) {
completion(result.data)
} else {
completion(nil)
}
}.resume()
}
Wrap the response in a DirectusResponse struct with a data property to match Directus’s JSON envelope.
Setting Up Geo-fences with CLCircularRegion
After fetching the fences, convert each into a CLCircularRegion and start monitoring.
Converting and Starting Monitoring
func startMonitoring(fences: [GeoFence]) {
for fence in fences {
let center = CLLocationCoordinate2D(latitude: fence.latitude, longitude: fence.longitude)
let region = CLCircularRegion(center: center,
radius: fence.radiusMeters,
identifier: fence.identifier)
region.notifyOnEntry = fence.notifyOnEntry
region.notifyOnExit = fence.notifyOnExit
locationManager.startMonitoring(for: region)
}
}
Call this method after successful API fetch, either on app launch or when the user’s general area changes. Avoid monitoring more than 20 regions simultaneously; filter to regions near the user’s current location if you have many fences.
Updating Fences Dynamically
Because your Directus collection is the single source of truth, you can add, remove, or deactivate fences without pushing an app update. Re-fetch periodically (e.g., every 30 minutes or at app resume) and reconcile with currently monitored regions. Remove any region that is no longer active using locationManager.stopMonitoring(for:).
Handling Region Entry and Exit Events
Implement the CLLocationManagerDelegate methods to react to fence crossings. The app receives these events even when terminated if you set up the proper UIBackgroundModes key in Info.plist (location).
Delegate Methods
extension YourViewController: CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
guard let circularRegion = region as? CLCircularRegion else { return }
// Look up the fence from a local cache (e.g., dictionary keyed by identifier)
if let fence = cachedFences[circularRegion.identifier] {
// Use fence.metadata to determine action
triggerAlert(for: fence)
}
}
func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
// Similar handling
}
func locationManager(_ manager: CLLocationManager, monitoringDidFailFor region: CLRegion?, withError error: Error) {
// Log error, possibly retry after delay
}
}
Always include a local cache of fence metadata (e.g., a dictionary mapping identifier to the GeoFence object) so you have immediate access to the payload without a network call.
Local Notifications from Geo-fence Events
Use UNUserNotificationCenter to present local notifications when a fence is crossed. This keeps the user engaged even if the app is in the background. Example inside the delegate method:
let content = UNMutableNotificationContent()
content.title = fence.name
content.body = fence.metadata?["message"] as? String ?? "You have arrived!"
let request = UNNotificationRequest(identifier: fence.identifier, content: content, trigger: nil)
UNUserNotificationCenter.current().add(request)
Best Practices for Production Geo-fencing with Directus
Shipping a reliable geo-fencing feature requires attention to battery life, data freshness, and edge cases.
Optimize Region Monitoring
- Limit region count: iOS allows a maximum of 20 monitored regions simultaneously. If your Directus collection contains more, filter to regions within a reasonable distance from the user’s last known location (e.g., using a geospatial query).
- Use larger radii initially: A 500–1000 meter radius reduces false triggers and conserves battery compared to 50-meter fences.
- Stop monitoring when not needed: Deactivate all regions if the user disables geo-fencing in your settings UI.
- Leverage significant-change location service: Use
startMonitoringSignificantLocationChanges()to periodically update the set of monitored fences without continuous GPS.
Handle Permissions Gracefully
If the user denies “Always” authorization, offer a simplified experience using “When In Use” or disable geo-fencing entirely. Remember that users can change permission at any time; listen to didChangeAuthorization status and adjust monitoring accordingly.
Keep Data in Sync
- Use Directus webhooks or the app’s background fetch to pull updated fences. A 15-minute interval is reasonable for most use cases.
- Include a
versionfield in your collection to detect changes and avoid re-processing identical data. - Store fetched fences locally (e.g., in Core Data or UserDefaults) as a fallback when the device is offline.
Test Thoroughly
Geo-fencing behaves differently on real devices vs simulators. Always test indoors/outdoors, with and without Wi-Fi. Use Xcode’s “Simulate Location” for basic validation, but verify with physical movement for accuracy.
Conclusion
Geo-fencing in iOS is a mature feature that, when paired with a flexible backend like Directus, becomes a powerful tool for creating responsive, location-aware applications. By storing geo-fence definitions in Directus, you decouple logic from the app binary and empower stakeholders to manage boundaries without developer intervention. From permissions and region monitoring to API integration and delegate callbacks, the combination gives you full control over both the client and server sides.
Start small: define a few fences in Directus, build a simple iOS client, and expand as you learn. With careful attention to battery management and data synchronization, your geo-fencing solution will scale gracefully across a fleet of devices.