// // 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? private var locationContinuation: CheckedContinuation? 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