Robustness: Run GeoMonitor on MainActor (#6)
* Robustness: Run GeoMonitor on MainActor * Test compile fix
This commit is contained in:
parent
18fcf08c36
commit
6bd6092fb4
|
@ -18,6 +18,7 @@ public protocol GeoMonitorDataSource {
|
||||||
/// - Monitoring a set of regions where the user wants to be alerted as they approach them, but
|
/// - 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)
|
/// monitoring is limited for brief durations (e.g., "get off here" alerts for transit apps)
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
|
@MainActor
|
||||||
public class GeoMonitor: NSObject, ObservableObject {
|
public class GeoMonitor: NSObject, ObservableObject {
|
||||||
enum Constants {
|
enum Constants {
|
||||||
static var currentLocationRegionMaximumRadius: CLLocationDistance = 2_500
|
static var currentLocationRegionMaximumRadius: CLLocationDistance = 2_500
|
||||||
|
@ -220,8 +221,10 @@ public class GeoMonitor: NSObject, ObservableObject {
|
||||||
locationManager.desiredAccuracy = desiredAccuracy
|
locationManager.desiredAccuracy = desiredAccuracy
|
||||||
locationManager.requestLocation()
|
locationManager.requestLocation()
|
||||||
|
|
||||||
fetchTimer = .scheduledTimer(withTimeInterval: Constants.currentLocationFetchTimeOut, repeats: false) { [unowned self] _ in
|
fetchTimer = .scheduledTimer(withTimeInterval: Constants.currentLocationFetchTimeOut, repeats: false) { [weak self] _ in
|
||||||
self.notify(.failure(LocationFetchError.noLocationFetchedInTime))
|
Task { [weak self] in
|
||||||
|
await self?.notify(.failure(LocationFetchError.noLocationFetchedInTime))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
@ -280,6 +283,8 @@ public class GeoMonitor: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func startMonitoring() {
|
public func startMonitoring() {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
guard !isMonitoring, hasAccess else { return }
|
guard !isMonitoring, hasAccess else { return }
|
||||||
|
|
||||||
isMonitoring = true
|
isMonitoring = true
|
||||||
|
@ -298,6 +303,8 @@ public class GeoMonitor: NSObject, ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func stopMonitoring() {
|
public func stopMonitoring() {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
guard isMonitoring else { return }
|
guard isMonitoring else { return }
|
||||||
|
|
||||||
isMonitoring = false
|
isMonitoring = false
|
||||||
|
@ -344,6 +351,8 @@ extension GeoMonitor {
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func runUpdateCycle(trigger: FetchTrigger) async -> CLLocation? {
|
func runUpdateCycle(trigger: FetchTrigger) async -> CLLocation? {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
// Re-monitor current area, so that it updates the data again
|
// Re-monitor current area, so that it updates the data again
|
||||||
// and also fetch current location at same time, to prioritise monitoring
|
// and also fetch current location at same time, to prioritise monitoring
|
||||||
// when we leave it.
|
// when we leave it.
|
||||||
|
@ -356,6 +365,8 @@ extension GeoMonitor {
|
||||||
}
|
}
|
||||||
|
|
||||||
func monitorCurrentArea() async throws -> CLLocation {
|
func monitorCurrentArea() async throws -> CLLocation {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
let location = try await fetchCurrentLocation()
|
let location = try await fetchCurrentLocation()
|
||||||
|
|
||||||
// Monitor a radius around it, using a single fixed "my location" circle
|
// Monitor a radius around it, using a single fixed "my location" circle
|
||||||
|
@ -399,11 +410,17 @@ extension GeoMonitor {
|
||||||
extension GeoMonitor {
|
extension GeoMonitor {
|
||||||
|
|
||||||
private func monitorDebounced(_ regions: [CLCircularRegion], location: CLLocation?, delay: TimeInterval? = nil) {
|
private func monitorDebounced(_ regions: [CLCircularRegion], location: CLLocation?, delay: TimeInterval? = nil) {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
|
// When this fires in the background we end up with many of these somehow
|
||||||
|
|
||||||
monitorTask?.cancel()
|
monitorTask?.cancel()
|
||||||
monitorTask = Task {
|
monitorTask = Task {
|
||||||
if let delay {
|
if let delay {
|
||||||
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
|
||||||
|
try Task.checkCancellation()
|
||||||
}
|
}
|
||||||
|
|
||||||
monitorNow(regions, location: location)
|
monitorNow(regions, location: location)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -411,6 +428,8 @@ extension GeoMonitor {
|
||||||
private func monitorNow(_ regions: [CLCircularRegion], location: CLLocation?) {
|
private func monitorNow(_ regions: [CLCircularRegion], location: CLLocation?) {
|
||||||
guard !Task.isCancelled else { return }
|
guard !Task.isCancelled else { return }
|
||||||
|
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
// Remember all the regions, if it currently too far away
|
// Remember all the regions, if it currently too far away
|
||||||
regionsToMonitor = regions
|
regionsToMonitor = regions
|
||||||
|
|
||||||
|
@ -444,7 +463,9 @@ extension GeoMonitor {
|
||||||
eventHandler(.status("Updating monitored regions. \(regions.count) candidates; monitoring \(toMonitor.count) regions; removed \(removedCount), kept \(monitoredAlready.count), added \(newRegion.count); now monitoring \(locationManager.monitoredRegions.count).", .updatingMonitoredRegions))
|
eventHandler(.status("Updating monitored regions. \(regions.count) candidates; monitoring \(toMonitor.count) regions; removed \(removedCount), kept \(monitoredAlready.count), added \(newRegion.count); now monitoring \(locationManager.monitoredRegions.count).", .updatingMonitoredRegions))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
static func determineRegionsToMonitor(regions: [CLCircularRegion], location: CLLocation?, max: Int) -> [CLCircularRegion] {
|
static func determineRegionsToMonitor(regions: [CLCircularRegion], location: CLLocation?, max: Int) -> [CLCircularRegion] {
|
||||||
|
|
||||||
let processed: [(CLCircularRegion, distance: CLLocationDistance?, priority: Int?)] = regions.map { region in
|
let processed: [(CLCircularRegion, distance: CLLocationDistance?, priority: Int?)] = regions.map { region in
|
||||||
let distance = location.map { $0.distance(from: .init(latitude: region.center.latitude, longitude: region.center.longitude)) }
|
let distance = location.map { $0.distance(from: .init(latitude: region.center.latitude, longitude: region.center.longitude)) }
|
||||||
let priority = (region as? PrioritizedRegion)?.priority
|
let priority = (region as? PrioritizedRegion)?.priority
|
||||||
|
@ -483,6 +504,8 @@ extension GeoMonitor {
|
||||||
extension GeoMonitor: CLLocationManagerDelegate {
|
extension GeoMonitor: CLLocationManagerDelegate {
|
||||||
|
|
||||||
public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
|
public func locationManager(_ manager: CLLocationManager, didEnterRegion region: CLRegion) {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
guard isMonitoring else {
|
guard isMonitoring else {
|
||||||
eventHandler(.status("GeoMonitor exited region, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
|
eventHandler(.status("GeoMonitor exited region, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
|
||||||
return
|
return
|
||||||
|
@ -524,6 +547,8 @@ extension GeoMonitor: CLLocationManagerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
|
public func locationManager(_ manager: CLLocationManager, didExitRegion region: CLRegion) {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
guard isMonitoring else {
|
guard isMonitoring else {
|
||||||
eventHandler(.status("GeoMonitor entered region, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
|
eventHandler(.status("GeoMonitor entered region, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
|
||||||
return
|
return
|
||||||
|
@ -540,6 +565,8 @@ extension GeoMonitor: CLLocationManagerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
|
public func locationManager(_ manager: CLLocationManager, didVisit visit: CLVisit) {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
guard isMonitoring else {
|
guard isMonitoring else {
|
||||||
eventHandler(.status("GeoMonitor detected visit change, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
|
eventHandler(.status("GeoMonitor detected visit change, even though we've since stopped monitoring. Ignoring...", .enteredRegion))
|
||||||
return
|
return
|
||||||
|
@ -563,6 +590,8 @@ extension GeoMonitor: CLLocationManagerDelegate {
|
||||||
print("GeoMonitor updated locations -> \(locations)")
|
print("GeoMonitor updated locations -> \(locations)")
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
guard let latest = locations.last else { return assertionFailure() }
|
guard let latest = locations.last else { return assertionFailure() }
|
||||||
|
|
||||||
guard let latestAccurate = locations
|
guard let latestAccurate = locations
|
||||||
|
@ -592,6 +621,8 @@ extension GeoMonitor: CLLocationManagerDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||||||
|
dispatchPrecondition(condition: .onQueue(.main))
|
||||||
|
|
||||||
updateAccess()
|
updateAccess()
|
||||||
askHandler(hasAccess)
|
askHandler(hasAccess)
|
||||||
askHandler = { _ in }
|
askHandler = { _ in }
|
||||||
|
|
|
@ -5,7 +5,7 @@ import CoreLocation
|
||||||
|
|
||||||
@available(iOS 14.0, *)
|
@available(iOS 14.0, *)
|
||||||
final class GeoMonitorTests: XCTestCase {
|
final class GeoMonitorTests: XCTestCase {
|
||||||
func testManyRegions() throws {
|
func testManyRegions() async throws {
|
||||||
// This is an example of a functional test case.
|
// This is an example of a functional test case.
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct
|
// Use XCTAssert and related functions to verify your tests produce the correct
|
||||||
// results.
|
// results.
|
||||||
|
@ -106,14 +106,14 @@ final class GeoMonitorTests: XCTestCase {
|
||||||
|
|
||||||
let needle = CLLocation(latitude: -31.9586, longitude: 115.8681)
|
let needle = CLLocation(latitude: -31.9586, longitude: 115.8681)
|
||||||
|
|
||||||
let withoutLocation = GeoMonitor.determineRegionsToMonitor(regions: regions, location: nil, max: 19)
|
let withoutLocation = await GeoMonitor.determineRegionsToMonitor(regions: regions, location: nil, max: 19)
|
||||||
XCTAssertEqual(withoutLocation.count, 19)
|
XCTAssertEqual(withoutLocation.count, 19)
|
||||||
XCTAssertFalse(withoutLocation.allSatisfy { needle.distance(from: .init(latitude: $0.center.latitude, longitude: $0.center.longitude)) <= 5_000 })
|
XCTAssertFalse(withoutLocation.allSatisfy { needle.distance(from: .init(latitude: $0.center.latitude, longitude: $0.center.longitude)) <= 5_000 })
|
||||||
XCTAssertEqual(withoutLocation.compactMap { $0 as? PrioritizedRegion }.map(\.priority).min() ?? 0, 529) // highest priorities
|
XCTAssertEqual(withoutLocation.compactMap { $0 as? PrioritizedRegion }.map(\.priority).min() ?? 0, 529) // highest priorities
|
||||||
XCTAssertEqual(withoutLocation.compactMap { $0 as? PrioritizedRegion }.map(\.priority).max(), 900)
|
XCTAssertEqual(withoutLocation.compactMap { $0 as? PrioritizedRegion }.map(\.priority).max(), 900)
|
||||||
XCTAssertEqual(withoutLocation.compactMap { $0 as? PrioritizedRegion }.filter { $0.priority == 900 }.count, 8) // all top priorities included
|
XCTAssertEqual(withoutLocation.compactMap { $0 as? PrioritizedRegion }.filter { $0.priority == 900 }.count, 8) // all top priorities included
|
||||||
|
|
||||||
let withLocation = GeoMonitor.determineRegionsToMonitor(regions: regions, location: needle, max: 19)
|
let withLocation = await GeoMonitor.determineRegionsToMonitor(regions: regions, location: needle, max: 19)
|
||||||
XCTAssertEqual(withLocation.count, 19)
|
XCTAssertEqual(withLocation.count, 19)
|
||||||
XCTAssertTrue(withLocation.allSatisfy { needle.distance(from: .init(latitude: $0.center.latitude, longitude: $0.center.longitude)) <= 5_000 })
|
XCTAssertTrue(withLocation.allSatisfy { needle.distance(from: .init(latitude: $0.center.latitude, longitude: $0.center.longitude)) <= 5_000 })
|
||||||
XCTAssertEqual(withLocation.compactMap { $0 as? PrioritizedRegion }.map(\.priority).min() ?? 0, 349) // highest priorities
|
XCTAssertEqual(withLocation.compactMap { $0 as? PrioritizedRegion }.map(\.priority).min() ?? 0, 349) // highest priorities
|
||||||
|
|
Loading…
Reference in New Issue