Add PunchPoint and LocationReport modules with home routing.
Migrate check-in point management and location reporting from placeholders to full MVVM flows, including foreground location support and unit tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
107
suixinkan/Core/Location/ForegroundLocationProvider.swift
Normal file
107
suixinkan/Core/Location/ForegroundLocationProvider.swift
Normal file
@ -0,0 +1,107 @@
|
||||
//
|
||||
// ForegroundLocationProvider.swift
|
||||
// suixinkan
|
||||
//
|
||||
// Created by Codex on 2026/6/24.
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
import Observation
|
||||
|
||||
/// 前台定位结果实体,表示一次即时定位得到的坐标和可展示地址。
|
||||
struct ForegroundLocationResult: Equatable {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
let address: String
|
||||
}
|
||||
|
||||
/// 前台定位 Provider,只负责当前页面主动请求位置,不缓存、不后台持续定位。
|
||||
@MainActor
|
||||
@Observable
|
||||
final class ForegroundLocationProvider: NSObject {
|
||||
@ObservationIgnored private let manager = CLLocationManager()
|
||||
@ObservationIgnored private let geocoder = CLGeocoder()
|
||||
@ObservationIgnored private var continuation: CheckedContinuation<ForegroundLocationResult, Error>?
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
manager.delegate = self
|
||||
manager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
}
|
||||
|
||||
/// 请求一次当前位置,成功后尝试反解析为地址。
|
||||
func requestCurrentLocation() async throws -> ForegroundLocationResult {
|
||||
if let continuation {
|
||||
continuation.resume(throwing: LocationProviderError.duplicatedRequest)
|
||||
self.continuation = nil
|
||||
}
|
||||
let status = manager.authorizationStatus
|
||||
if status == .notDetermined {
|
||||
manager.requestWhenInUseAuthorization()
|
||||
}
|
||||
|
||||
return try await withCheckedThrowingContinuation { continuation in
|
||||
self.continuation = continuation
|
||||
manager.requestLocation()
|
||||
}
|
||||
}
|
||||
|
||||
/// 将 CLLocation 转为页面可展示的定位结果。
|
||||
private func makeResult(from location: CLLocation) async -> ForegroundLocationResult {
|
||||
let address: String
|
||||
if let placemark = try? await geocoder.reverseGeocodeLocation(location).first {
|
||||
address = [
|
||||
placemark.administrativeArea,
|
||||
placemark.locality,
|
||||
placemark.subLocality,
|
||||
placemark.thoroughfare,
|
||||
placemark.name
|
||||
]
|
||||
.compactMap { $0 }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined(separator: "")
|
||||
} else {
|
||||
address = "当前位置"
|
||||
}
|
||||
return ForegroundLocationResult(
|
||||
latitude: location.coordinate.latitude,
|
||||
longitude: location.coordinate.longitude,
|
||||
address: address.isEmpty ? "当前位置" : address
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension ForegroundLocationProvider: CLLocationManagerDelegate {
|
||||
/// 定位成功回调,返回最近一次坐标。
|
||||
nonisolated func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||
guard let location = locations.last else { return }
|
||||
Task { @MainActor in
|
||||
guard let continuation = self.continuation else { return }
|
||||
self.continuation = nil
|
||||
let result = await self.makeResult(from: location)
|
||||
continuation.resume(returning: result)
|
||||
}
|
||||
}
|
||||
|
||||
/// 定位失败回调,将错误传给当前请求方。
|
||||
nonisolated func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||
Task { @MainActor in
|
||||
guard let continuation = self.continuation else { return }
|
||||
self.continuation = nil
|
||||
continuation.resume(throwing: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 前台定位 Provider 错误实体。
|
||||
enum LocationProviderError: LocalizedError {
|
||||
case duplicatedRequest
|
||||
|
||||
var errorDescription: String? {
|
||||
switch self {
|
||||
case .duplicatedRequest:
|
||||
"已有定位请求正在执行"
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user