mirror of
https://github.com/jayanezj/traccar-client-macos.git
synced 2026-11-05 00:00:45 +01:00
Add explicit permission handling, independent continuous and single capture, location status and deterministic CI coverage. Final decisions and validation: https://github.com/jayanezj/traccar-client-macos/issues/4#issuecomment-6399189792
159 lines
6.1 KiB
Swift
159 lines
6.1 KiB
Swift
import CoreLocation
|
|
import Foundation
|
|
|
|
@MainActor
|
|
final class CoreLocationProvider: LocationProviding {
|
|
private(set) var availability: LocationAvailability
|
|
var onEvent: ((LocationEvent) -> Void)?
|
|
|
|
private let permissionManager: CLLocationManager
|
|
private var permissionDelegate: NativeLocationDelegate?
|
|
private var continuousManager: CLLocationManager?
|
|
private var continuousDelegate: NativeLocationDelegate?
|
|
private var singleManager: CLLocationManager?
|
|
private var singleDelegate: NativeLocationDelegate?
|
|
private var availabilityRevision = 0
|
|
private var continuousRequest: Int?
|
|
private var singleRequest: Int?
|
|
|
|
init() {
|
|
permissionManager = CLLocationManager()
|
|
availability = LocationAvailability(
|
|
permission: LocationPermission(permissionManager.authorizationStatus), services: .checking
|
|
)
|
|
permissionDelegate = NativeLocationDelegate { [weak self] _ in self?.refreshAvailability() }
|
|
permissionManager.delegate = permissionDelegate
|
|
refreshAvailability()
|
|
}
|
|
|
|
func refreshAvailability() {
|
|
availability.permission = LocationPermission(permissionManager.authorizationStatus)
|
|
onEvent?(.availability(availability))
|
|
availabilityRevision += 1
|
|
let revision = availabilityRevision
|
|
Task { [weak self] in
|
|
// The synchronous service-status query may block; do not perform it on the UI thread.
|
|
let enabled = await Task.detached { CLLocationManager.locationServicesEnabled() }.value
|
|
guard let self, revision == availabilityRevision else { return }
|
|
availability.services = enabled ? .enabled : .disabled
|
|
onEvent?(.availability(availability))
|
|
}
|
|
}
|
|
|
|
func requestAuthorization() { permissionManager.requestWhenInUseAuthorization() }
|
|
|
|
func startContinuous(accuracy: LocationAccuracy, request: Int) {
|
|
stopContinuous()
|
|
let manager = CLLocationManager()
|
|
continuousManager = manager
|
|
continuousRequest = request
|
|
continuousDelegate = makeDelegate(kind: .continuous, request: request)
|
|
manager.delegate = continuousDelegate
|
|
manager.desiredAccuracy = Self.desiredAccuracy(accuracy)
|
|
manager.distanceFilter = kCLDistanceFilterNone
|
|
manager.startUpdatingLocation()
|
|
}
|
|
|
|
func stopContinuous() {
|
|
continuousRequest = nil
|
|
continuousManager?.stopUpdatingLocation()
|
|
continuousManager?.delegate = nil
|
|
continuousManager = nil
|
|
continuousDelegate = nil
|
|
}
|
|
|
|
func requestSingle(accuracy: LocationAccuracy, request: Int) {
|
|
cancelSingle()
|
|
let manager = CLLocationManager()
|
|
singleManager = manager
|
|
singleRequest = request
|
|
singleDelegate = makeDelegate(kind: .single, request: request)
|
|
manager.delegate = singleDelegate
|
|
manager.desiredAccuracy = Self.desiredAccuracy(accuracy)
|
|
manager.requestLocation()
|
|
}
|
|
|
|
func cancelSingle() {
|
|
singleRequest = nil
|
|
singleManager?.stopUpdatingLocation()
|
|
singleManager?.delegate = nil
|
|
singleManager = nil
|
|
singleDelegate = nil
|
|
}
|
|
|
|
static func desiredAccuracy(_ accuracy: LocationAccuracy) -> CLLocationAccuracy {
|
|
switch accuracy {
|
|
case .low: kCLLocationAccuracyKilometer
|
|
case .medium: kCLLocationAccuracyHundredMeters
|
|
case .high: kCLLocationAccuracyBest
|
|
}
|
|
}
|
|
|
|
private func makeDelegate(kind: LocationRequestKind, request: Int) -> NativeLocationDelegate {
|
|
NativeLocationDelegate { [weak self] event in
|
|
guard let self else { return }
|
|
switch event {
|
|
case .authorization:
|
|
refreshAvailability()
|
|
case .locations(let locations):
|
|
deliver(locations, kind: kind, request: request)
|
|
case .failure(let error):
|
|
deliver(error, kind: kind, request: request)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func deliver(_ locations: [CLLocation], kind: LocationRequestKind, request: Int) {
|
|
guard isCurrent(kind, request: request) else { return }
|
|
let fixes = locations.compactMap(LocationFix.init)
|
|
if fixes.isEmpty {
|
|
onEvent?(.failure(kind, request: request, reason: .unavailable, fallback: nil))
|
|
} else {
|
|
onEvent?(.locations(kind, request: request, fixes: fixes))
|
|
}
|
|
if kind == .single && singleRequest == request { cancelSingle() }
|
|
}
|
|
|
|
private func deliver(_ error: any Error, kind: LocationRequestKind, request: Int) {
|
|
guard isCurrent(kind, request: request) else { return }
|
|
let reason: LocationFailure
|
|
switch (error as? CLError)?.code {
|
|
case .denied: reason = .permissionDenied
|
|
case .locationUnknown: reason = .unavailable
|
|
default: reason = .other((error as NSError).code)
|
|
}
|
|
let fallback = singleManager?.location.flatMap(LocationFix.init)
|
|
onEvent?(.failure(kind, request: request, reason: reason, fallback: fallback))
|
|
if kind == .single && singleRequest == request { cancelSingle() }
|
|
if reason == .permissionDenied { refreshAvailability() }
|
|
}
|
|
|
|
private func isCurrent(_ kind: LocationRequestKind, request: Int) -> Bool {
|
|
kind == .single ? singleRequest == request : continuousRequest == request
|
|
}
|
|
}
|
|
|
|
private enum NativeLocationEvent {
|
|
case authorization
|
|
case locations([CLLocation])
|
|
case failure(any Error)
|
|
}
|
|
|
|
// CLLocationManager is created on MainActor and delivers delegates on that thread's run loop.
|
|
@MainActor
|
|
private final class NativeLocationDelegate: NSObject, @preconcurrency CLLocationManagerDelegate {
|
|
private let receive: (NativeLocationEvent) -> Void
|
|
|
|
init(receive: @escaping (NativeLocationEvent) -> Void) { self.receive = receive }
|
|
|
|
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { receive(.authorization) }
|
|
|
|
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
receive(.locations(locations))
|
|
}
|
|
|
|
func locationManager(_ manager: CLLocationManager, didFailWithError error: any Error) {
|
|
receive(.failure(error))
|
|
}
|
|
}
|