Gym Visits (Geofencing)
Gym Visits uses geofencing to detect when a member enters, dwells at, or exits a verified facility. The SDK registers geofences for the member's facilities and emits events that your app can use to show notifications, log visits, or update the UI.
UI Reference
Display each facility as a card showing the gym name, address, visit count, distance, and monitoring status. Active facilities show a green "Monitoring" badge.
Key data mappings:
| UI Element | AFCore Source |
|---|---|
| Facility name & address | facilities().get() → Facility.name, .address, .city, .state |
| Visit count | activities().get(month, year) filtered by GYM_VISIT_ACTIVITY and facility ID |
| Monitoring status | facilities().isMonitoringActive() + geofence event stream |
| Geofence badge | Subscribe to facilities().subscribeToEvents() for Registered / Unregistered |
How It Works
- Permissions -- Grant foreground location, background location, and notification permissions.
- Facilities -- Load the member's verified locations. Facilities can also be added from Map search results.
- Start monitoring -- Register geofences with the OS via
startMonitoring(). - Listen for events -- Subscribe to
facilities().subscribeToEvents()to react to entry, exit, dwell, and error events. - Notifications -- Post local notifications when geofence events occur.
Platform Requirements
Android
- Create a notification channel before posting geofence notifications.
- Google Play Services must be available. If unavailable, geofencing will not function.
- No additional manifest or service configuration is needed -- AFCore's library manifest includes the boot receiver, broadcast receiver, and all required permissions.
iOS
- Add
Info.plistkeys:NSLocationWhenInUseUsageDescription,NSLocationAlwaysAndWhenInUseUsageDescription. - Enable Background Modes > Location Updates in your Xcode project capabilities.
- The member must grant Always Allow location access for geofencing to work while the app is in the background.
- No background task registration is needed for geofencing. iOS region monitoring is system-level and survives app termination and reboots automatically.
Requesting Permissions
Use AFPermissions to check and request permissions. Request foreground location first, then escalate to background location, then notifications. Always explain why each permission is needed before prompting.
- Android (Kotlin)
- iOS (Swift)
// 1. Foreground location
if (AFPermissions.getPermissionStatus(Permission.FINE_LOCATION) != PermissionStatus.GRANTED) {
AFPermissions.requestPermission(Permission.FINE_LOCATION)
}
// 2. Background location (Android 10+; request only after foreground is granted)
if (AFPermissions.getPermissionStatus(Permission.BACKGROUND_LOCATION) != PermissionStatus.GRANTED) {
AFPermissions.requestPermission(Permission.BACKGROUND_LOCATION)
}
// 3. Notifications (Android 13+)
if (AFPermissions.getPermissionStatus(Permission.NOTIFICATIONS) != PermissionStatus.GRANTED) {
AFPermissions.requestPermission(Permission.NOTIFICATIONS)
}
If any permission is permanently denied (RESTRICTED), show an in-app dialog explaining why it is needed and deep-link the user to the system Settings screen.
// 1. Foreground location
if try await AFPermissions.shared.getPermissionStatus(permission: .fineLocation) != .granted {
try await AFPermissions.shared.requestPermission(permission: .fineLocation)
}
// 2. Background location (escalate to Always Allow)
if try await AFPermissions.shared.getPermissionStatus(permission: .backgroundLocation) != .granted {
try await AFPermissions.shared.requestPermission(permission: .backgroundLocation)
}
// 3. Notifications
if try await AFPermissions.shared.getPermissionStatus(permission: .notifications) != .granted {
try await AFPermissions.shared.requestPermission(permission: .notifications)
}
If a permission is denied or restricted, show an alert with a button that opens Settings via AFPermissions.shared.openAppSettings().
Start Monitoring
After permissions are granted and the member has verified facilities, start geofence monitoring.
- Android (Kotlin)
- iOS (Swift)
lifecycleScope.launch {
try {
AFCore.facilities().startMonitoring()
Log.d("GymVisits", "Geofence monitoring started")
} catch (e: Exception) {
Log.e("GymVisits", "Failed to start monitoring: ${e.message}")
}
}
do {
try await AFCore.shared.facilities().startMonitoring()
print("Geofence monitoring started")
} catch {
print("Failed to start monitoring: \(error)")
}
Geofence Events
Subscribe to geofence events via facilities().subscribeToEvents() to receive real-time geofence updates. The geofenceId in each event corresponds to the facility's facilityId.
Event Types
| Event | Fields | Description |
|---|---|---|
Registered | geofenceIds: List<String> | Geofences were successfully registered with the OS. |
Unregistered | geofenceIds: List<String>? | Geofences were removed. null means all were cleared. |
Entered | geofenceId: String, location: AFGeoLocation? | The device entered a geofence. Marks the start of a potential visit. |
Exited | geofenceId: String, location: AFGeoLocation? | The device left a geofence. |
Dwell | geofenceId: String, location: AFGeoLocation? | The device remained inside long enough to qualify as a visit. |
Heartbeat | location: AFGeoLocation? | Periodic location update for diagnostics (Android only). |
Error | throwable: Throwable | An error occurred within the geofencing service. |
AFGeoLocation contains latitude, longitude, accuracy, and timestamp. It represents raw coordinates and should not be confused with a Facility.
- Android (Kotlin)
- iOS (Swift)
val subscription = AFCore.facilities().subscribeToEvents(
onEvent = { event ->
when (event) {
is AFGeofenceEvent.Entered ->
showNotification("Visit Started", "You arrived at facility ${event.geofenceId}")
is AFGeofenceEvent.Dwell ->
showNotification("Visit Confirmed", "Your visit has been recorded")
is AFGeofenceEvent.Exited ->
showNotification("Visit Ended", "You left facility ${event.geofenceId}")
is AFGeofenceEvent.Error ->
Log.e("GymVisits", "Geofence error: ${event.throwable.message}")
else ->
Log.d("GymVisits", "Event: $event")
}
},
onError = { error ->
Log.e("GymVisits", "Event stream error: ${error.message}")
}
)
// When no longer needed:
subscription.close()
private var geofenceSubscription: FlowSubscription?
func startListening() {
geofenceSubscription = AFCore.shared.facilities().subscribeToEvents(
onEvent: { event in
DispatchQueue.main.async {
self.postLocalNotification(
title: "Geofencing Event",
body: String(describing: event)
)
}
},
onError: { error in
print("Geofence error: \(error)")
}
)
}
func stopListening() {
geofenceSubscription?.close()
geofenceSubscription = nil
}
Stop Monitoring
Stop geofence monitoring when the member logs out or no longer needs visit detection.
- Android (Kotlin)
- iOS (Swift)
AFCore.facilities().stopMonitoring()
try await AFCore.shared.facilities().stopMonitoring()
Background Survival (App Kill and Reboot)
Geofencing continues to function after the app is terminated or the device reboots. Each platform handles this differently.
Android
Google Play Services clears all registered geofences on device reboot and app update. AFCore handles this automatically:
- Geofence persistence -- When geofences are registered, AFCore saves them to local storage.
- Boot receiver -- After reboot or app update,
AFGeofenceBootReceiverreads the saved geofences and re-registers them with Google Play Services. - AFCore re-initialization -- The boot receiver automatically re-initializes AFCore from persisted configuration, so network calls and event reporting work without the user opening the app.
Consumer app action: None required. The boot receiver and persistence are built into the SDK.
iOS
iOS preserves CLLocationManager region monitoring across app termination and reboots -- the OS automatically re-launches the app in the background when a monitored region is entered or exited.
- Region monitoring -- iOS maintains registered regions even after process kill. When an event occurs, the system relaunches the app and delivers the event to
AFGeofenceLocationDelegate. - AFCore re-initialization -- When the app is relaunched in the background, AFCore automatically re-initializes from persisted configuration stored in
NSUserDefaults. No consumer action is required.
Consumer app action: None required. Region monitoring is managed entirely by iOS at the system level. No BGTaskScheduler registration is needed for geofencing.
Best Practices
- Request permissions in context. Explain why background location is needed before prompting. Request background location only after foreground access is granted.
- Respect platform geofence limits. Android supports up to 100 geofences per app; iOS supports approximately 20 monitored regions. Prioritize the member's most frequently visited facilities.
- Create the notification channel early (Android). Set up the channel during app initialization, before any geofence events can fire.
- Use a foreground service on Android for reliable detection while the app is in the background.
- Handle Google Play Services unavailability. Check for availability and show an actionable error if Play Services is missing or outdated.
Troubleshooting
| Symptom | Likely Cause | Solution |
|---|---|---|
| No events fired | Missing permissions | Verify foreground + background location and notifications are granted. Check that device location services are enabled. |
| Only some geofences register | Platform limit exceeded | Reduce the number of monitored facilities. Inspect the Error event stream for details. |
| App killed stops detection (Android) | No foreground service | Use a foreground service when the app needs continuous monitoring. Consider requesting battery optimization exemption. |
| No background events (iOS) | "When In Use" only | Confirm the member granted "Always Allow." Ensure region monitoring is available on the device. |
| No events after reboot (Android) | Boot receiver issue | Verify the device has Google Play Services. Check logcat for AFGeofenceBootReceiver messages. |
| No events after app kill (iOS) | Missing "Always Allow" | Confirm the member granted "Always Allow" location. Region monitoring requires this to receive background events. |
| Geofences lost after app update (Android) | Normal behavior | AFCore automatically re-registers geofences via the boot receiver after MY_PACKAGE_REPLACED. No action needed. |
Quick Reference
- Android (Kotlin)
- iOS (Swift)
AFPermissions.getPermissionStatus(Permission.FINE_LOCATION)
AFPermissions.requestPermission(Permission.FINE_LOCATION)
AFPermissions.requestPermission(Permission.BACKGROUND_LOCATION)
AFPermissions.requestPermission(Permission.NOTIFICATIONS)
AFCore.facilities().startMonitoring()
AFCore.facilities().stopMonitoring()
AFCore.facilities().isMonitoringActive()
AFCore.facilities().subscribeToEvents(onEvent, onError) // FlowSubscription
try await AFPermissions.shared.getPermissionStatus(permission: .fineLocation)
try await AFPermissions.shared.requestPermission(permission: .fineLocation)
try await AFPermissions.shared.requestPermission(permission: .backgroundLocation)
try await AFPermissions.shared.requestPermission(permission: .notifications)
try await AFCore.shared.facilities().startMonitoring()
try await AFCore.shared.facilities().stopMonitoring()
try await AFCore.shared.facilities().isMonitoringActive()
AFCore.shared.facilities().subscribeToEvents(onEvent:onError:) // FlowSubscription