Gym Visits (Geofencing)
Workflow Overview
- Permissions — Location (foreground and background) and Notifications must be granted before monitoring.
- Facilities — Retrieve and manage the member’s verified locations. Facilities can be added by searching nearby places.
- Geofencing Lifecycle — Start monitoring, receive geofence events, and stop monitoring when appropriate.
- Notifications — Local alerts inform members when geofence events occur.
- Troubleshooting & Error Handling — Ensure permissions are correct and handle errors gracefully.
Setup and Requirements
Android
- Ensure a notification channel is created before posting notifications.
- If Google Play Services are not available and no fallback is used, geofencing will not function.
iOS
- Add Info.plist keys:
NSLocationWhenInUseUsageDescription,NSLocationAlwaysAndWhenInUseUsageDescription, and notification usage descriptions. - Enable Background Modes → Location Updates.
- Request and ensure Always Allow location access for geofencing to function correctly.
Requesting Permissions
Use the internal AFPermissions expect object to check and request permissions. On both platforms, handle foreground location, background location, and notifications separately. Always provide a brief rationale before a permission prompt and a clear path to Settings when the user has denied.
Android (Kotlin)
// Foreground Location
if (!AFPermissions.isPermissionGranted(Permission.FINE_LOCATION)) {
// Rationale (optional based on shouldShowRequestPermissionRationale):
// "We use your location to detect visits at your verified facilities."
AFPermissions.requestPermission(Permission.FINE_LOCATION)
}
// Background Location (Android 10+; must be requested after foreground is granted)
if (!AFPermissions.isPermissionGranted(Permission.BACKGROUND_LOCATION)) {
// Rationale:
// "Always allow location so automatic gym visits work even when the app is closed."
AFPermissions.requestPermission(Permission.BACKGROUND_LOCATION)
}
// Notifications (Android 13+)
if (!AFPermissions.isPermissionGranted(Permission.NOTIFICATIONS)) {
// Rationale:
// "We’ll notify you when a visit starts or completes."
AFPermissions.requestPermission(Permission.NOTIFICATIONS)
}
// If any permission is permanently denied, present an in‑app dialog explaining why it is needed
// and deep‑link the user to App Settings to enable it.
iOS (Swift)
// Foreground location (When In Use)
if try await !AFPermissions.shared.isPermissionGranted(permission: .locationWhenInUse) {
// Rationale: "We use your location to detect visits at your verified facilities."
try await AFPermissions.shared.requestPermission(permission: .locationWhenInUse)
}
// Escalate to Always Allow
if try await !AFPermissions.shared.isPermissionGranted(permission: .locationAlways) {
// Rationale: "Choose 'Always Allow' so geofencing works while the app is closed."
try await AFPermissions.shared.requestPermission(permission: .locationAlways)
}
// Notifications
if try await !AFPermissions.shared.isPermissionGranted(permission: .notifications) {
// Rationale: "We’ll notify you when a visit starts or completes."
try await AFPermissions.shared.requestPermission(permission: .notifications)
}
// If a permission is denied, show an alert with a button that opens Settings (UIApplication.openSettingsURLString).
Turning On Geofencing
Android (Kotlin)
scope.launch {
runCatching {
AFCore.facilities().startMonitoring()
viewModel.setResult("Started monitoring verified facilities")
}.onFailure { err ->
viewModel.setResult("Failed to start monitoring: ${err.message}")
}
}
iOS (Swift)
Task {
do {
try await AFCore.shared.facilities().startMonitoring()
print("Started monitoring verified facilities")
} catch {
print("Failed to start monitoring: \(error)")
}
}
Listening for Events
These are the events emitted by AFGeofencing.events. The geofenceId is always the same as the FacilityId.
AFGeoLocation represents the raw geographic coordinates (latitude, longitude, accuracy, timestamp) and should not be confused with a Facility.
| Event | When it Fires | Fields | Notes |
|---|---|---|---|
Registered | After one or more geofences are successfully registered with the OS | geofenceIds: List<String> (FacilityIds) | Confirms monitoring started for specific facilities. |
Unregistered | After geofences are removed | geofenceIds: List<String>? (FacilityIds or null for all) | Confirms monitoring stopped. |
Entered | When the device enters a geofence | geofenceId: String (FacilityId), location: AFGeoLocation? | Start of a visit. |
Exited | When the device exits a geofence | geofenceId: String (FacilityId), location: AFGeoLocation? | End of a visit if dwell not reached. |
Dwell | When the device remained inside long enough to count as a visit | geofenceId: String (FacilityId), location: AFGeoLocation? | Fires after configured dwell time is reached. |
Heartbeat | Periodic update of the device’s current location | location: AFGeoLocation? | Useful for diagnostics. |
Error | When an error occurs within the geofencing service | throwable: Throwable | Inspect for details and recovery. |
Android (Kotlin)
LaunchedEffect(Unit) {
AFGeofencing.events.collect { event ->
viewModel.setResult("Geofencing event: $event")
ensureGeofenceChannel(context)
showGeofenceNotification(context, "Geofencing Event", event.toString())
}
}
iOS (Swift) — Option A: Callbacks (no Swift concurrency)
// Keep a strong ref so the stream stays alive
private var geofenceSub: FlowSubscription?
func startGeofenceCallbacks() {
geofenceSub = AFGeofencing.shared.subscribeToGeofencingEvents(
onEvent: { event in
let text = String(describing: event)
DispatchQueue.main.async {
postLocalNotification(title: "Geofencing Event", body: text)
}
},
onError: { err in
DispatchQueue.main.async {
postLocalNotification(title: "Geofence Error", body: err.localizedDescription)
}
},
onComplete: {
DispatchQueue.main.async {
postLocalNotification(title: "Geofencing", body: "Stream completed")
}
}
)
}
func stopGeofenceCallbacks() {
geofenceSub?.close()
geofenceSub = nil
}
iOS (Swift) — Option B: Swift Concurrency (AsyncSequence)
func startGeofenceAsyncSequence() {
Task { @MainActor in
for await event in AFGeofencing.shared.events {
postLocalNotification(title: "Geofencing Event", body: String(describing: event))
}
}
}
Best Practices
- Ask permissions in context: Explain why background location and notifications are needed; request background location only after foreground is granted.
- Keep geofence count reasonable: Android API limit per request is 100; iOS practical limit is ~20 monitored regions. Batch/register accordingly.
- Validate inputs: Clamp lat/lon, radius (100–1000m on iOS; 75–1000m Android), and dwell (60–3600s) before registering.
- Foreground service (Android): Use a foreground service and motion-aware heartbeats to maintain reliability while conserving battery.
- Handle Google Play Services: Detect unavailability and emit actionable errors.
- Status & permissions helpers: Surface
AFGeofencing.getStatus()andhasPermissions()to drive UI state. - Notifications channel (Android): Create the channel before posting any notifications.
- Uninstall/reinstall: Android encrypted prefs are cleared on uninstall; iOS Keychain persists—use a reinstall sentinel (file) to clear secrets on first launch after reinstall.
- Logging: Avoid logging PII/coordinates at high frequency; prefer debug-level and redact where possible.
Troubleshooting
- No events fired: Check that required permissions are granted (foreground + background location, notifications). Verify device location services are enabled.
- Only some geofences register: Respect platform limits; validate data; inspect error stream for batch failures.
- App killed on Android: Ensure foreground service is active when moving; consider battery optimizations exceptions where appropriate.
- iOS not entering in background: Confirm
Alwaysauthorization is granted; region monitoring is available; avoid too-small radii.