Geofence¶
public structGeofence
A geofence definition for monitoring circular or polygon regions.
The SDK implements the native iOS and Android Geofencing APIs, extended with polygon support and a proximity-based infinite-geofencing system that overcomes the platform limits of 20 (iOS) and 100 (Android) simultaneous geofences.
Contents¶
- Overview
- Adding geofences
- Listening for events
- Polygon geofencing
- Infinite geofencing
- Removing geofences
- Geofences-only mode
- Examples
Overview¶
| Field | Required | Description |
|---|---|---|
identifier |
✅ | Unique name for this geofence. |
latitude |
✅* | Center latitude (*omit for polygon geofences). |
longitude |
✅* | Center longitude (*omit for polygon geofences). |
radius |
✅* | Radius in meters (*omit for polygon geofences). |
notifyOnEntry |
— | Fire event on entry. |
notifyOnExit |
— | Fire event on exit. |
notifyOnDwell |
— | Fire event after loitering for loiteringDelay ms. |
vertices |
— | Polygon geofence vertices (replaces lat/lng/radius). |
extras |
— | Arbitrary key-value metadata posted with each event. |
Warning
Both platforms require GeolocationConfig.locationAuthorizationRequest to be
"Always". Geofencing does not work with "WhenInUse" only.
Adding geofences¶
Use BGGeo.addGeofence for a single geofence, or BGGeo.addGeofences for bulk inserts (approximately 10× faster than inserting individually).
If a geofence with the same identifier already exists in the
database, it is replaced.
Listening for events¶
Subscribe to geofence transitions with BGGeo.onGeofence. Subscribe to changes in the actively monitored set with BGGeo.onGeofencesChange.
let bgGeo = BGGeo.shared
// Listen for geofence events.
let sub = bgGeo.onGeofence { geofence in
print("[geofence] \(geofence.identifier) \(geofence.action)")
}
Note
When all geofences are removed, BGGeo.onGeofencesChange
fires with empty arrays for both on and off.
Polygon geofencing¶
The SDK supports polygon geofences of any shape via the vertices
field. Polygon geofencing is
sold as a separate add-on
but is fully functional in DEBUG builds.
When defining a polygon geofence, omit latitude, longitude,
and radius — the SDK calculates the minimum enclosing circle
automatically from the polygon geometry.

let bgGeo = BGGeo.shared
let sub = bgGeo.onGeofencesChange { event in
let on = event.on // <-- new geofences activated.
let off = event.off // <-- geofences that were just de-activated.
// Create map circles
on.forEach { geofence in
print("[geofencesChange] activated: \(geofence.identifier)")
}
// Remove map circles
off.forEach { identifier in
print("[geofencesChange] deactivated: \(identifier)")
}
}
Infinite geofencing¶
The SDK stores all geofences in its database and uses a geospatial query to activate only the nearest geofences within GeolocationConfig.geofenceProximityRadius, staying within the platform limit. As the device moves, the active set is periodically refreshed.
- The minimum GeolocationConfig.geofenceProximityRadius is enforced at
1000meters. - Geofences within the radius (green in the diagram below) are actively monitored.
- Geofences outside the radius (grey) remain in the database but are dormant.

Removing geofences¶
Geofences persist in the SDK's database until explicitly removed. If
AppConfig.stopOnTerminate is false and
AppConfig.startOnBoot is true, geofences continue to be
monitored across app termination and device reboots.
let bgGeo = BGGeo.shared
Task {
do {
try await bgGeo.geofences.remove("HOME")
print("[removeGeofence] success")
} catch {
print("[removeGeofence] FAILURE: \(error)")
}
}
let bgGeo = BGGeo.shared
Task {
do {
try await bgGeo.geofences.removeAll()
print("[removeGeofences] all geofences have been destroyed")
} catch {
print("[removeGeofences] FAILURE: \(error)")
}
}
let bgGeo = BGGeo.shared
let geofences = bgGeo.geofences.getAll()
print("[getGeofences] \(geofences)")
Geofences-only mode¶
Call BGGeo.startGeofences instead of BGGeo.start to monitor geofences without continuous location tracking. Use GeolocationConfig.geofenceModeHighAccuracy to improve event responsiveness at the cost of higher power usage.
The SDK can switch between full tracking and geofences-only mode at any time by calling the corresponding start method.
let bgGeo = BGGeo.shared
let sub = bgGeo.onGeofence { geofence in
print("[geofence] \(geofence)")
}
bgGeo.ready { config in
config.http.url = "http://your.server.com/geofences"
config.http.autoSync = true
}
Task {
do {
// engage geofences-only mode:
try await bgGeo.startGeofences()
} catch {
print("[startGeofences] error: \(error)")
}
}
Examples¶
let bgGeo = BGGeo.shared
// Listen to geofence events
let sub = bgGeo.onGeofence { geofence in
print("[geofence] \(geofence)")
if geofence.identifier == "DANGER_ZONE" {
if geofence.action == "ENTER" {
// Entering the danger-zone, we want to aggressively track location.
Task { try await bgGeo.start() }
} else if geofence.action == "EXIT" {
// Exiting the danger-zone, we resume geofences-only tracking.
Task { try await bgGeo.startGeofences() }
}
}
}
// Add a geofence.
let geofence = Geofence(
identifier: "DANGER_ZONE",
radius: 1000,
latitude: 45.51921926,
longitude: -73.61678581,
notifyOnEntry: true,
notifyOnExit: true
)
Task {
try await bgGeo.geofences.add(geofence)
}
// Ready the plugin.
bgGeo.ready { config in
config.geolocation.desiredAccuracy = kCLLocationAccuracyBest
config.geolocation.distanceFilter = 10
config.http.url = "http://your.server.com/locations"
config.http.autoSync = true
}
Task {
try await bgGeo.startGeofences()
}
Single geofence¶
let bgGeo = BGGeo.shared
let geofence = Geofence(
identifier: "Home",
radius: 200,
latitude: 45.51921926,
longitude: -73.61678581,
notifyOnEntry: true,
notifyOnExit: true,
extras: ["route_id": 1234]
)
Task {
do {
try await bgGeo.geofences.add(geofence)
print("[addGeofence] success")
} catch {
print("[addGeofence] FAILURE: \(error)")
}
}
Multiple geofences¶
let bgGeo = BGGeo.shared
let geofences = [
Geofence(
identifier: "Home",
radius: 200,
latitude: 45.51921926,
longitude: -73.61678581,
notifyOnEntry: true
),
Geofence(
identifier: "Work",
radius: 200,
latitude: 45.61921927,
longitude: -73.71678582,
notifyOnEntry: true
)
]
Task {
do {
try await bgGeo.geofences.addAll(geofences)
print("[addGeofences] success")
} catch {
print("[addGeofences] FAILURE: \(error)")
}
}
Members¶
entryState¶
public let entryState: EntryState
Current entry state of the geofence.
| Value | State |
|---|---|
0 |
Outside |
1 |
Inside |
extras¶
Arbitrary key-value metadata attached to each geofence event and included in the payload posted to HttpConfig.url.
hits¶
Number of times this geofence has been triggered since it was added.
identifier¶
Unique identifier for this geofence.
Used to reference the geofence in events and removal calls. Adding a geofence with an identifier that already exists replaces the existing one.
latitude¶
Latitude of the circular geofence center.
Omit when defining a polygon geofence via vertices — the SDK
calculates the center automatically.
loiteringDelay¶
Minimum time in milliseconds the device must remain inside the geofence
before a notifyOnDwell event fires. Default 0.
longitude¶
Longitude of the circular geofence center.
Omit when defining a polygon geofence via vertices — the SDK
calculates the center automatically.
notifyOnDwell¶
Fire a GeofenceEvent when the device has remained inside this
geofence for loiteringDelay milliseconds.
notifyOnEntry¶
Fire a GeofenceEvent when the device enters this geofence.
See also - GeolocationConfig.geofenceInitialTriggerEntry
notifyOnExit¶
Fire a GeofenceEvent when the device exits this geofence.
radius¶
Radius of the circular geofence in meters.
Omit when defining a polygon geofence via vertices — the SDK
calculates the enclosing radius automatically.
Warning
The minimum reliable radius is 200 meters. Below this threshold,
geofences may not trigger reliably on either platform.
Apple documents this explicitly:
"For testing purposes, you can assume that the minimum distance is approximately 200 meters."
stateUpdatedAt¶
Epoch timestamp in seconds of the last geofence transition.
vertices¶
Polygon geofence vertices as [[lat, lng], ...] pairs.
When provided, the geofence is treated as a polygon rather than a circle.
Omit latitude, longitude, and radius — the SDK
solves the minimum enclosing circle
from the vertex geometry and registers that as the native circular geofence.
When the device enters the enclosing circle, the SDK begins C++ hit-testing against the polygon at high frequency. When it exits the circle, polygon monitoring ceases.
let bgGeo = BGGeo.shared
let geofence = Geofence(
identifier: "Home",
vertices: [
[45.518947279987714, -73.6049889209514], // <-- [lat, lng]
[45.5182711292279, -73.60338649600598],
[45.517082240237634, -73.60432670908212],
[45.51774871402813, -73.60604928622278]
],
notifyOnEntry: true,
notifyOnExit: true
)
Task {
do {
try await bgGeo.geofences.add(geofence)
} catch {
print("[addGeofence] FAILURE: \(error)")
}
}