Error handling robustness

- Don't fetch if we know we don't have access
- Allow fetching current location in parallel

Also: Comments and README info
This commit is contained in:
Adrian Schoenig 2022-05-27 17:33:07 +10:00
parent 7ccc0c911c
commit bf411c9c40
2 changed files with 33 additions and 18 deletions

View File

@ -11,7 +11,10 @@ Relies on a mixture of techniques, such as:
## Setup ## Setup
TODO: 1. Configure your *Target*:
- Go to *Signing & Capabilities*, *Background Modes* and make sure *Location updates* is ticket.
- Go to *Info*, and make sure you have usage descriptions for "Privacy - Location Always", "Privacy - Location Always and When in Use", and "Privacy - Location When In Use" set.
## Usage ## Usage

View File

@ -28,6 +28,8 @@ public class GeoMonitor: NSObject, ObservableObject {
} }
public enum LocationFetchError: Error { public enum LocationFetchError: Error {
case accessNotProvided
/// Happens if you stop monitoring before a location could be found /// Happens if you stop monitoring before a location could be found
case noLocationFetchedInTime case noLocationFetchedInTime
@ -59,6 +61,10 @@ public class GeoMonitor: NSObject, ObservableObject {
public var maxRegionsToMonitor = 20 public var maxRegionsToMonitor = 20
/// Instantiates new monitor
/// - Parameters:
/// - fetch: Handler that's called when the monitor decides it's a good time to update the regions to monitor. Should fetch and then return all regions to be monitored (even if they didn't change).
/// - onEvent: Handler that's called when a relevant event is happening, including when one of the monitored regions is entered.
public convenience init(fetch: @escaping (GeoMonitor.FetchTrigger) async -> [CLCircularRegion], onEvent: @escaping (Event) -> Void) { public convenience init(fetch: @escaping (GeoMonitor.FetchTrigger) async -> [CLCircularRegion], onEvent: @escaping (Event) -> Void) {
self.init(dataSource: SimpleDataSource(handler: fetch), onEvent: onEvent) self.init(dataSource: SimpleDataSource(handler: fetch), onEvent: onEvent)
} }
@ -156,7 +162,7 @@ public class GeoMonitor: NSObject, ObservableObject {
private var isMonitoring: Bool = false private var isMonitoring: Bool = false
private var withNextLocation: ((Result<CLLocation, Error>) -> Void)? = nil private var withNextLocation: [(Result<CLLocation, Error>) -> Void] = []
private var currentLocationRegion: CLRegion? = nil private var currentLocationRegion: CLRegion? = nil
@ -173,7 +179,7 @@ public class GeoMonitor: NSObject, ObservableObject {
} }
public func startMonitoring() { public func startMonitoring() {
guard !isMonitoring else { return } guard !isMonitoring, hasAccess else { return }
isMonitoring = true isMonitoring = true
locationManager.allowsBackgroundLocationUpdates = true locationManager.allowsBackgroundLocationUpdates = true
@ -207,21 +213,19 @@ public class GeoMonitor: NSObject, ObservableObject {
} }
private func fetchCurrentLocation() async throws -> CLLocation { private func fetchCurrentLocation() async throws -> CLLocation {
if let existing = withNextLocation { guard hasAccess else {
assertionFailure() throw LocationFetchError.accessNotProvided
existing(.failure(LocationFetchError.noLocationFetchedInTime))
withNextLocation = nil
} }
let originalAccuracy = locationManager.desiredAccuracy let originalAccuracy = locationManager.desiredAccuracy
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.requestLocation() locationManager.requestLocation()
return try await withCheckedThrowingContinuation { continuation in return try await withCheckedThrowingContinuation { continuation in
withNextLocation = { [unowned self] result in withNextLocation.append({ [unowned self] result in
self.locationManager.desiredAccuracy = originalAccuracy self.locationManager.desiredAccuracy = originalAccuracy
continuation.resume(with: result) continuation.resume(with: result)
} })
} }
} }
} }
@ -276,8 +280,10 @@ extension GeoMonitor {
} }
func stopMonitoringCurrentArea() { func stopMonitoringCurrentArea() {
withNextLocation?(.failure(LocationFetchError.noLocationFetchedInTime)) withNextLocation.forEach {
withNextLocation = nil $0(.failure(LocationFetchError.noLocationFetchedInTime))
}
withNextLocation = []
currentLocationRegion = nil currentLocationRegion = nil
} }
} }
@ -400,15 +406,19 @@ extension GeoMonitor: CLLocationManagerDelegate {
.filter({ $0.horizontalAccuracy <= manager.desiredAccuracy }) .filter({ $0.horizontalAccuracy <= manager.desiredAccuracy })
.last .last
else { else {
withNextLocation?(.failure(LocationFetchError.locationInaccurate(latest))) withNextLocation.forEach {
withNextLocation = nil $0(.failure(LocationFetchError.locationInaccurate(latest)))
}
withNextLocation = []
return return
} }
self.currentLocation = latestAccurate self.currentLocation = latestAccurate
withNextLocation?(.success(latestAccurate)) withNextLocation.forEach {
withNextLocation = nil $0(.success(latestAccurate))
}
withNextLocation = []
} }
public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) { public func locationManagerDidPauseLocationUpdates(_ manager: CLLocationManager) {
@ -429,8 +439,10 @@ extension GeoMonitor: CLLocationManagerDelegate {
print("GeoMonitor's location manager failed: \(error)") print("GeoMonitor's location manager failed: \(error)")
#endif #endif
withNextLocation?(.failure(error)) withNextLocation.forEach {
withNextLocation = nil $0(.failure(error))
}
withNextLocation = []
} }
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {