Refine background modes:

- Set `allowsBackgroundLocationUpdates` immediately, to get the blue
  pill when using "When in use" permissions.
- Avoid duplicate location checks when triggering `checkIfInRegion`
- Don't monitor regions when `!isMonitoring` and ignore callbacks
  after setting `isMonitoring = false`

Also: Comments
This commit is contained in:
Adrian Schoenig 2022-08-18 17:31:12 +10:00
parent 7f11da6b83
commit ab1aa0051f
2 changed files with 46 additions and 8 deletions

View File

@ -1,6 +1,6 @@
# GeoMonitor
A battery-efficient and privacy-preserving toolkit for monitoring the user's
A battery-efficient and privacy-friendly mini framework for monitoring the user's
location, triggering callbacks when the user starts moving and monitoring
whether the user approaches specified regions.

View File

@ -8,6 +8,14 @@ public protocol GeoMonitorDataSource {
}
/// Monitors the user's current location and triggers events when entering previously registered
/// regions; also stays up-to-date by checking for new regions whenever the user moves significantly.
///
/// Typical use cases:
/// - Monitoring dynamic regions that are of some relevant to the user and where the user wants to be
/// alerted, when they get to them (e.g., traffic incidents); where monitoring can be long-term.
/// - Monitoring a set of regions where the user wants to be alerted as they approach them, but
/// monitoring is limited for brief durations (e.g., "get off here" alerts for transit apps)
public class GeoMonitor: NSObject, ObservableObject {
enum Constants {
static var currentLocationRegionMaximumRadius: CLLocationDistance = 2_500
@ -94,6 +102,7 @@ public class GeoMonitor: NSObject, ObservableObject {
super.init()
locationManager.delegate = self
locationManager.allowsBackgroundLocationUpdates = true
#if !DEBUG
locationManager.activityType = .automotiveNavigation
@ -102,7 +111,7 @@ public class GeoMonitor: NSObject, ObservableObject {
// MARK: - Access
/// Whether user has granted any kind of access to the device's location
/// Whether user has granted any kind of access to the device's location, when-in-use or always
@Published public var hasAccess: Bool
private var askHandler: (Bool) -> Void = { _ in }
@ -225,6 +234,8 @@ public class GeoMonitor: NSObject, ObservableObject {
// MARK: - Current region monitoring
/// Whether background monitoring is currently enabled
///
/// - warning: Setting this will prompt for access to the user's location with always-on tracking.
@Published public var enableInBackground: Bool = false {
didSet {
guard enableInBackground != oldValue else { return }
@ -265,9 +276,9 @@ public class GeoMonitor: NSObject, ObservableObject {
locationManager.pausesLocationUpdatesAutomatically = enableInBackground // we can do that, as it implies "always on" permissions
Task {
// It's okay for this to fail, but best to enable visit monitoring as
// a backup.
try? await monitorCurrentArea()
// Check if in region, which will also re-monitor the current location
// and update the regions(!)
await checkIfInRegion()
}
if enableVisitMonitoring {
@ -287,10 +298,12 @@ public class GeoMonitor: NSObject, ObservableObject {
}
public func update(regions: [CLCircularRegion]) async {
let location = try? await fetchCurrentLocation()
let location = isMonitoring ? (try? await fetchCurrentLocation()) : nil
monitor(regions, location: location)
}
/// Trigger a check whether the user is in any of the registered regions and, if so, trigger the primary
/// event handler (with case `.manual`).
public func checkIfInRegion() async {
guard let location = await runUpdateCycle(trigger: .manual) else { return }
@ -368,6 +381,12 @@ extension GeoMonitor {
extension GeoMonitor {
func monitor(_ regions: [CLCircularRegion], location: CLLocation?) {
// Remember all the regions, if it currently too far away
regionsToMonitor = regions
guard isMonitoring else { return }
// Then effectively monitor the nearest
let nearby: [CLCircularRegion]
if let currentLocation = location {
nearby = regions.filter { region in
@ -378,8 +397,6 @@ extension GeoMonitor {
nearby = regions
}
regionsToMonitor = nearby
// The ones to monitor, optionally pruned by the nearest
let toMonitor: [CLCircularRegion]
@ -420,6 +437,13 @@ extension GeoMonitor {
extension GeoMonitor: CLLocationManagerDelegate {
public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
guard isMonitoring else {
#if DEBUG
eventHandler(.debug("GeoMonitor exited region, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
#endif
return
}
guard region.identifier != currentLocationRegion?.identifier else {
return // Ignore re-entering current region; we only care about exiting this
}
@ -464,6 +488,13 @@ extension GeoMonitor: CLLocationManagerDelegate {
}
public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
guard isMonitoring else {
#if DEBUG
eventHandler(.debug("GeoMonitor entered region, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
#endif
return
}
guard currentLocationRegion?.identifier == region.identifier else {
return // Ignore exiting a monitored region; we only care about entering these.
}
@ -475,6 +506,13 @@ extension GeoMonitor: CLLocationManagerDelegate {
}
public func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
guard isMonitoring else {
#if DEBUG
eventHandler(.debug("GeoMonitor detected visit change, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
#endif
return
}
#if DEBUG
if visit.departureDate == .distantFuture {
eventHandler(.debug("GeoMonitor visit arrival -> \(visit)", .visitMonitoring))