273 lines
9.9 KiB
Swift
273 lines
9.9 KiB
Swift
//
|
||
// LocationProvider.swift
|
||
// suixinkan
|
||
//
|
||
|
||
import CoreLocation
|
||
import Foundation
|
||
|
||
/// Core Location 管理器最小接口,便于模拟器定位实现注入测试替身。
|
||
@MainActor
|
||
protocol CoreLocationManaging: AnyObject {
|
||
var authorizationStatus: CLAuthorizationStatus { get }
|
||
var desiredAccuracy: CLLocationAccuracy { get set }
|
||
var delegate: CLLocationManagerDelegate? { get set }
|
||
|
||
/// 请求使用期间定位权限。
|
||
func requestWhenInUseAuthorization()
|
||
/// 请求一次当前位置。
|
||
func requestLocation()
|
||
}
|
||
|
||
extension CLLocationManager: CoreLocationManaging {}
|
||
|
||
/// Core Location 定位实现,供模拟器运行并支持依赖注入测试。
|
||
@MainActor
|
||
final class CoreLocationProvider: NSObject, LocationProviding, CLLocationManagerDelegate {
|
||
|
||
nonisolated static let shared = CoreLocationProvider()
|
||
|
||
private var appStore: AppStore = .shared
|
||
private var manager: any CoreLocationManaging = CLLocationManager()
|
||
private var geocoder: any LocationAddressGeocoding = LocationGeocoder.shared
|
||
private var permissionContinuation: CheckedContinuation<Void, Error>?
|
||
private var locationContinuation: CheckedContinuation<CLLocationCoordinate2D, Error>?
|
||
|
||
nonisolated override init() {
|
||
super.init()
|
||
}
|
||
|
||
/// 使用可控依赖创建 Core Location 定位服务。
|
||
init(
|
||
appStore: AppStore,
|
||
manager: any CoreLocationManaging,
|
||
geocoder: any LocationAddressGeocoding
|
||
) {
|
||
self.appStore = appStore
|
||
self.manager = manager
|
||
self.geocoder = geocoder
|
||
super.init()
|
||
manager.delegate = self
|
||
}
|
||
|
||
/// 请求坐标及逆地理编码地址。
|
||
func requestSnapshot(
|
||
desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest
|
||
) async throws -> HomeLocationSnapshot {
|
||
let coordinate = try await requestCoordinate(desiredAccuracy: desiredAccuracy)
|
||
let address = await geocoder.reverseGeocode(
|
||
latitude: coordinate.latitude,
|
||
longitude: coordinate.longitude
|
||
)
|
||
return HomeLocationSnapshot(
|
||
latitude: coordinate.latitude,
|
||
longitude: coordinate.longitude,
|
||
address: address
|
||
)
|
||
}
|
||
|
||
/// 请求一次当前位置坐标。
|
||
func requestCoordinate(
|
||
desiredAccuracy: CLLocationAccuracy
|
||
) async throws -> CLLocationCoordinate2D {
|
||
try requirePrivacyAgreement()
|
||
try await ensureLocationPermission()
|
||
guard locationContinuation == nil else {
|
||
throw LocationProviderError.locationFailed
|
||
}
|
||
|
||
manager.delegate = self
|
||
manager.desiredAccuracy = desiredAccuracy
|
||
return try await withCheckedThrowingContinuation { continuation in
|
||
locationContinuation = continuation
|
||
manager.requestLocation()
|
||
}
|
||
}
|
||
|
||
/// 使用系统服务将坐标解析为地址。
|
||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||
guard appStore.session.privacyAgreementAccepted else { return "" }
|
||
return await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||
}
|
||
|
||
/// 请求系统定位权限,等待用户完成首次授权。
|
||
func ensureLocationPermission() async throws {
|
||
manager.delegate = self
|
||
switch manager.authorizationStatus {
|
||
case .authorizedAlways, .authorizedWhenInUse:
|
||
return
|
||
case .denied, .restricted:
|
||
throw LocationProviderError.permissionDenied
|
||
case .notDetermined:
|
||
guard permissionContinuation == nil else {
|
||
throw LocationProviderError.locationFailed
|
||
}
|
||
try await withCheckedThrowingContinuation { continuation in
|
||
permissionContinuation = continuation
|
||
manager.requestWhenInUseAuthorization()
|
||
}
|
||
@unknown default:
|
||
throw LocationProviderError.permissionDenied
|
||
}
|
||
}
|
||
|
||
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
||
resolvePermission(status: self.manager.authorizationStatus)
|
||
}
|
||
|
||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||
guard let continuation = locationContinuation else { return }
|
||
locationContinuation = nil
|
||
guard let coordinate = locations.last?.coordinate,
|
||
CLLocationCoordinate2DIsValid(coordinate),
|
||
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||
return
|
||
}
|
||
continuation.resume(returning: coordinate)
|
||
}
|
||
|
||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||
guard let continuation = locationContinuation else { return }
|
||
locationContinuation = nil
|
||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||
}
|
||
|
||
private func requirePrivacyAgreement() throws {
|
||
guard appStore.session.privacyAgreementAccepted else {
|
||
throw LocationProviderError.privacyNotAccepted
|
||
}
|
||
}
|
||
|
||
private func resolvePermission(status: CLAuthorizationStatus) {
|
||
guard let continuation = permissionContinuation else { return }
|
||
switch status {
|
||
case .authorizedAlways, .authorizedWhenInUse:
|
||
permissionContinuation = nil
|
||
continuation.resume()
|
||
case .denied, .restricted:
|
||
permissionContinuation = nil
|
||
continuation.resume(throwing: LocationProviderError.permissionDenied)
|
||
case .notDetermined:
|
||
break
|
||
@unknown default:
|
||
permissionContinuation = nil
|
||
continuation.resume(throwing: LocationProviderError.permissionDenied)
|
||
}
|
||
}
|
||
}
|
||
|
||
#if targetEnvironment(simulator)
|
||
|
||
/// 模拟器默认定位服务,使用 Core Location 与系统逆地理编码。
|
||
typealias LocationProvider = CoreLocationProvider
|
||
|
||
#else
|
||
|
||
/// 真机统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
||
@MainActor
|
||
final class LocationProvider: NSObject, LocationProviding {
|
||
|
||
nonisolated static let shared = LocationProvider()
|
||
|
||
private let permissionManager = CLLocationManager()
|
||
private let geocoder = LocationGeocoder.shared
|
||
|
||
nonisolated override init() {
|
||
super.init()
|
||
}
|
||
|
||
/// 请求坐标及高德逆地理编码地址。
|
||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
||
try AMapBootstrap.requireConfigured()
|
||
try await ensureLocationPermission()
|
||
|
||
let manager = makeLocationManager(
|
||
desiredAccuracy: desiredAccuracy,
|
||
locatingWithReGeocode: true
|
||
)
|
||
return try await withCheckedThrowingContinuation { continuation in
|
||
let started = manager.requestLocation(withReGeocode: true) { location, regeocode, error in
|
||
if let error {
|
||
continuation.resume(throwing: error)
|
||
return
|
||
}
|
||
guard let location else {
|
||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||
return
|
||
}
|
||
let address = regeocode?.formattedAddress ?? ""
|
||
continuation.resume(returning: HomeLocationSnapshot(
|
||
latitude: location.coordinate.latitude,
|
||
longitude: location.coordinate.longitude,
|
||
address: address
|
||
))
|
||
}
|
||
if !started {
|
||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 请求一次当前位置坐标。
|
||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
||
try AMapBootstrap.requireConfigured()
|
||
try await ensureLocationPermission()
|
||
|
||
let manager = makeLocationManager(
|
||
desiredAccuracy: desiredAccuracy,
|
||
locatingWithReGeocode: false
|
||
)
|
||
return try await withCheckedThrowingContinuation { continuation in
|
||
let started = manager.requestLocation(withReGeocode: false) { location, _, error in
|
||
if let error {
|
||
continuation.resume(throwing: error)
|
||
return
|
||
}
|
||
guard let location else {
|
||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||
return
|
||
}
|
||
continuation.resume(returning: location.coordinate)
|
||
}
|
||
if !started {
|
||
continuation.resume(throwing: LocationProviderError.locationFailed)
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 使用高德服务将坐标解析为地址。
|
||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||
}
|
||
|
||
/// 请求系统定位权限,首次弹窗后等待用户响应。
|
||
func ensureLocationPermission() async throws {
|
||
permissionManager.desiredAccuracy = kCLLocationAccuracyBest
|
||
let initialStatus = permissionManager.authorizationStatus
|
||
if initialStatus == .notDetermined {
|
||
permissionManager.requestWhenInUseAuthorization()
|
||
try await Task.sleep(nanoseconds: 500_000_000)
|
||
}
|
||
|
||
let status = permissionManager.authorizationStatus
|
||
guard status == .authorizedWhenInUse || status == .authorizedAlways else {
|
||
throw LocationProviderError.permissionDenied
|
||
}
|
||
}
|
||
|
||
private func makeLocationManager(
|
||
desiredAccuracy: CLLocationAccuracy,
|
||
locatingWithReGeocode: Bool
|
||
) -> AMapLocationManager {
|
||
let manager = AMapLocationManager()
|
||
manager.desiredAccuracy = desiredAccuracy
|
||
manager.locationTimeout = 30
|
||
manager.reGeocodeTimeout = 10
|
||
manager.locatingWithReGeocode = locatingWithReGeocode
|
||
return manager
|
||
}
|
||
}
|
||
|
||
#endif
|