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 version field 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.