新增打卡点与位置上报模块,并接入首页路由

将打卡点管理与位置上报从占位页迁移为完整 MVVM 流程,包含前台定位支持与单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-24 11:08:41 +08:00
parent 403a3eefa6
commit abcac9bfdf
22 changed files with 2698 additions and 12 deletions

View 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:
"已有定位请求正在执行"
}
}
}