集成高德 SDK 并替换所有定位相关的 CoreLocation 与 MapKit

This commit is contained in:
2026-07-07 11:49:22 +08:00
parent 3acbf6315b
commit 8a4a30111c
29 changed files with 748 additions and 240 deletions

View File

@ -0,0 +1,49 @@
//
// AMapBootstrap.swift
// suixinkan
//
import Foundation
/// SDK Key SDK
enum AMapBootstrap {
private(set) static var isConfigured = false
private static let lock = NSLock()
/// App SDK
@discardableResult
static func configureIfNeeded(appStore: AppStore = .shared) -> Bool {
guard appStore.privacyAgreementAccepted else { return false }
lock.lock()
defer { lock.unlock() }
guard !isConfigured else { return true }
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapLocationManager.updatePrivacyAgree(.didAgree)
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
MAMapView.updatePrivacyAgree(.didAgree)
AMapSearchAPI.updatePrivacyShow(.didShow, privacyInfo: .didContain)
AMapSearchAPI.updatePrivacyAgree(.didAgree)
AMapServices.shared().apiKey = AMapConfig.apiKey
AMapServices.shared().enableHTTPS = true
isConfigured = true
return true
}
/// SDK
static func requireConfigured(appStore: AppStore = .shared) throws {
guard configureIfNeeded(appStore: appStore) else {
throw LocationProviderError.privacyNotAccepted
}
}
///
static func resetForTesting() {
lock.lock()
defer { lock.unlock() }
isConfigured = false
}
}

View File

@ -0,0 +1,64 @@
//
// LocationGeocoder.swift
// suixinkan
//
import Foundation
/// Android `GeocodeSearch`
final class LocationGeocoder: NSObject, AMapSearchDelegate {
static let shared = LocationGeocoder()
private var searchAPI: AMapSearchAPI?
private var continuation: CheckedContinuation<String, Never>?
private override init() {
super.init()
}
///
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
do {
try AMapBootstrap.requireConfigured()
} catch {
return ""
}
return await withCheckedContinuation { continuation in
self.continuation?.resume(returning: "")
self.continuation = continuation
guard let api = makeSearchAPI() else {
continuation.resume(returning: "")
return
}
let request = AMapReGeocodeSearchRequest()
request.location = AMapGeoPoint.location(withLatitude: CGFloat(latitude), longitude: CGFloat(longitude))
request.requireExtension = true
request.radius = 200
api.aMapReGoecodeSearch(request)
}
}
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
let address = response?.regeocode?.formattedAddress ?? ""
continuation?.resume(returning: address)
continuation = nil
}
func aMapSearchRequest(_ request: Any!, didFailWithError error: Error!) {
continuation?.resume(returning: "")
continuation = nil
}
private func makeSearchAPI() -> AMapSearchAPI? {
if let searchAPI {
return searchAPI
}
guard let api = AMapSearchAPI() else { return nil }
api.delegate = self
searchAPI = api
return api
}
}

View File

@ -0,0 +1,100 @@
//
// LocationProvider.swift
// suixinkan
//
import CoreLocation
import Foundation
/// `AMapLocationManager` Android `LocationProvider`
final class LocationProvider: NSObject, LocationProviding {
static let shared = LocationProvider()
private let permissionManager = CLLocationManager()
private let geocoder: LocationGeocoder
init(geocoder: LocationGeocoder = .shared) {
self.geocoder = geocoder
super.init()
permissionManager.desiredAccuracy = kCLLocationAccuracyBest
}
func requestSnapshot() async throws -> HomeLocationSnapshot {
try AMapBootstrap.requireConfigured()
try await ensureLocationPermission()
let manager = makeLocationManager()
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() async throws -> CLLocationCoordinate2D {
try AMapBootstrap.requireConfigured()
try await ensureLocationPermission()
let manager = makeLocationManager()
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 {
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() -> AMapLocationManager {
let manager = AMapLocationManager()
manager.desiredAccuracy = kCLLocationAccuracyBest
manager.locationTimeout = 30
manager.reGeocodeTimeout = 10
manager.locatingWithReGeocode = true
return manager
}
}

View File

@ -0,0 +1,54 @@
//
// LocationProviding.swift
// suixinkan
//
import CoreLocation
import Foundation
///
struct HomeLocationSnapshot: Sendable, Equatable {
let latitude: Double
let longitude: Double
let address: String
}
/// 便 ViewModel mock
protocol LocationProviding: Sendable {
///
func requestSnapshot() async throws -> HomeLocationSnapshot
///
func requestCoordinate() async throws -> CLLocationCoordinate2D
///
func reverseGeocode(latitude: Double, longitude: Double) async -> String
}
///
enum LocationProviderError: LocalizedError, Equatable {
case privacyNotAccepted
case permissionDenied
case locationFailed
var errorDescription: String? {
switch self {
case .privacyNotAccepted:
"请先阅读并同意隐私政策"
case .permissionDenied:
"需要定位权限才能使用此功能"
case .locationFailed:
"定位失败,请稍后重试"
}
}
}
///
enum OrderLocationError: LocalizedError {
case permissionDenied
var errorDescription: String? {
switch self {
case .permissionDenied:
"缺少定位权限,功能无法进行"
}
}
}