feat: 支持模拟器定位与地图 fallback
This commit is contained in:
@ -20,6 +20,7 @@ enum AMapBootstrap {
|
||||
defer { lock.unlock() }
|
||||
guard !isConfigured else { return true }
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
AMapLocationManager.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||
AMapLocationManager.updatePrivacyAgree(.didAgree)
|
||||
MAMapView.updatePrivacyShow(.didShow, privacyInfo: .didContain)
|
||||
@ -29,6 +30,7 @@ enum AMapBootstrap {
|
||||
|
||||
AMapServices.shared().apiKey = AMapConfig.apiKey
|
||||
AMapServices.shared().enableHTTPS = true
|
||||
#endif
|
||||
isConfigured = true
|
||||
return true
|
||||
}
|
||||
|
||||
@ -3,16 +3,28 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
/// 高德逆地理编码封装,对齐 Android `GeocodeSearch`。
|
||||
/// 地址逆地理编码能力,供平台定位实现注入。
|
||||
@MainActor
|
||||
final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
||||
protocol LocationAddressGeocoding: AnyObject {
|
||||
/// 将坐标解析为可读地址,失败时返回空字符串。
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||
}
|
||||
|
||||
/// 平台逆地理编码封装;真机使用高德,模拟器使用 Core Location。
|
||||
@MainActor
|
||||
final class LocationGeocoder: NSObject, LocationAddressGeocoding {
|
||||
|
||||
nonisolated static let shared = LocationGeocoder()
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
private let geocoder = CLGeocoder()
|
||||
#else
|
||||
private var searchAPI: AMapSearchAPI?
|
||||
private var continuation: CheckedContinuation<String, Never>?
|
||||
#endif
|
||||
|
||||
nonisolated private override init() {
|
||||
super.init()
|
||||
@ -20,6 +32,26 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
||||
|
||||
/// 将坐标解析为可读地址。
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
#if targetEnvironment(simulator)
|
||||
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||
guard let placemark = try? await geocoder.reverseGeocodeLocation(location).first else {
|
||||
return ""
|
||||
}
|
||||
if let name = placemark.name?.trimmingCharacters(in: .whitespacesAndNewlines),
|
||||
!name.isEmpty {
|
||||
return name
|
||||
}
|
||||
return [
|
||||
placemark.administrativeArea,
|
||||
placemark.locality,
|
||||
placemark.subLocality,
|
||||
placemark.thoroughfare,
|
||||
placemark.subThoroughfare,
|
||||
]
|
||||
.compactMap { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.joined()
|
||||
#else
|
||||
do {
|
||||
try AMapBootstrap.requireConfigured()
|
||||
} catch {
|
||||
@ -40,8 +72,10 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
||||
request.radius = 200
|
||||
api.aMapReGoecodeSearch(request)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
func onReGeocodeSearchDone(_ request: AMapReGeocodeSearchRequest!, response: AMapReGeocodeSearchResponse!) {
|
||||
let address = response?.regeocode?.formattedAddress ?? ""
|
||||
continuation?.resume(returning: address)
|
||||
@ -62,4 +96,9 @@ final class LocationGeocoder: NSObject, AMapSearchDelegate {
|
||||
searchAPI = api
|
||||
return api
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !targetEnvironment(simulator)
|
||||
extension LocationGeocoder: AMapSearchDelegate {}
|
||||
#endif
|
||||
|
||||
@ -6,7 +6,165 @@
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
/// 统一定位服务,封装高德 `AMapLocationManager`,对齐 Android `LocationProvider`。
|
||||
/// 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 {
|
||||
|
||||
@ -19,7 +177,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
||||
super.init()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 请求坐标及高德逆地理编码地址。
|
||||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy = kCLLocationAccuracyBest) async throws -> HomeLocationSnapshot {
|
||||
try AMapBootstrap.requireConfigured()
|
||||
try await ensureLocationPermission()
|
||||
@ -51,7 +209,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 请求一次当前位置坐标。
|
||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D {
|
||||
try AMapBootstrap.requireConfigured()
|
||||
try await ensureLocationPermission()
|
||||
@ -78,7 +236,7 @@ final class LocationProvider: NSObject, LocationProviding {
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
/// 使用高德服务将坐标解析为地址。
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String {
|
||||
await geocoder.reverseGeocode(latitude: latitude, longitude: longitude)
|
||||
}
|
||||
@ -110,3 +268,5 @@ final class LocationProvider: NSObject, LocationProviding {
|
||||
return manager
|
||||
}
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@ -16,20 +16,25 @@ struct HomeLocationSnapshot: Sendable, Equatable {
|
||||
/// 定位服务协议,便于 ViewModel 注入与单元测试 mock。
|
||||
protocol LocationProviding: Sendable {
|
||||
/// 按指定精度请求当前坐标与地址。
|
||||
@MainActor
|
||||
func requestSnapshot(desiredAccuracy: CLLocationAccuracy) async throws -> HomeLocationSnapshot
|
||||
/// 按指定精度请求当前坐标。
|
||||
@MainActor
|
||||
func requestCoordinate(desiredAccuracy: CLLocationAccuracy) async throws -> CLLocationCoordinate2D
|
||||
/// 逆地理编码。
|
||||
@MainActor
|
||||
func reverseGeocode(latitude: Double, longitude: Double) async -> String
|
||||
}
|
||||
|
||||
extension LocationProviding {
|
||||
/// 请求当前坐标与地址,默认使用最高精度以保持既有业务行为。
|
||||
@MainActor
|
||||
func requestSnapshot() async throws -> HomeLocationSnapshot {
|
||||
try await requestSnapshot(desiredAccuracy: kCLLocationAccuracyBest)
|
||||
}
|
||||
|
||||
/// 请求当前坐标,默认使用最高精度以保持既有业务行为。
|
||||
@MainActor
|
||||
func requestCoordinate() async throws -> CLLocationCoordinate2D {
|
||||
try await requestCoordinate(desiredAccuracy: kCLLocationAccuracyBest)
|
||||
}
|
||||
|
||||
@ -3,6 +3,7 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import UIKit
|
||||
|
||||
/// 主 Tab 中间扫码按钮结果处理,对齐 Android `MainTabViewModel.signIn`。
|
||||
|
||||
@ -7,6 +7,10 @@ import CoreLocation
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
/// 地图缩放/定位控件。
|
||||
final class LocationReportMapControlStack: UIView {
|
||||
|
||||
@ -295,7 +299,112 @@ final class LocationReportBottomActionView: UIControl {
|
||||
}
|
||||
}
|
||||
|
||||
/// 地图容器,封装高德 `MAMapView`。
|
||||
/// 位置上报地图容器,模拟器使用 MapKit。
|
||||
#if targetEnvironment(simulator)
|
||||
final class LocationReportMapView: UIView, MKMapViewDelegate {
|
||||
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
let mapView: MKMapView
|
||||
private var markerAnnotation: MKPointAnnotation?
|
||||
private var shouldCenterOnNextUserLocation = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
mapView = MKMapView(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.showsCompass = false
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
mapView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 进入页面后,在首次拿到用户位置时将地图居中。
|
||||
func enableInitialUserLocationCentering() {
|
||||
shouldCenterOnNextUserLocation = true
|
||||
centerOnUserLocationIfNeeded()
|
||||
}
|
||||
|
||||
func centerOnUserLocation() {
|
||||
guard let coordinate = validUserCoordinate else { return }
|
||||
setCenter(coordinate)
|
||||
}
|
||||
|
||||
func setCenter(_ coordinate: CLLocationCoordinate2D, animated: Bool = true) {
|
||||
let region = MKCoordinateRegion(
|
||||
center: coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.012, longitudeDelta: 0.012)
|
||||
)
|
||||
mapView.setRegion(region, animated: animated)
|
||||
}
|
||||
|
||||
func zoomIn() {
|
||||
updateZoom(scale: 0.5)
|
||||
}
|
||||
|
||||
func zoomOut() {
|
||||
updateZoom(scale: 2)
|
||||
}
|
||||
|
||||
func updateMarker(latitude: Double?, longitude: Double?) {
|
||||
if let markerAnnotation {
|
||||
mapView.removeAnnotation(markerAnnotation)
|
||||
self.markerAnnotation = nil
|
||||
}
|
||||
guard let latitude, let longitude else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude)
|
||||
markerAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||
centerOnUserLocationIfNeeded()
|
||||
}
|
||||
|
||||
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||
guard gesture.state == .ended else { return }
|
||||
let point = gesture.location(in: mapView)
|
||||
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||
}
|
||||
|
||||
private var validUserCoordinate: CLLocationCoordinate2D? {
|
||||
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||
CLLocationCoordinate2DIsValid(coordinate),
|
||||
coordinate.latitude != 0 || coordinate.longitude != 0 else {
|
||||
return nil
|
||||
}
|
||||
return coordinate
|
||||
}
|
||||
|
||||
private func centerOnUserLocationIfNeeded() {
|
||||
guard shouldCenterOnNextUserLocation, let coordinate = validUserCoordinate else { return }
|
||||
setCenter(coordinate, animated: false)
|
||||
shouldCenterOnNextUserLocation = false
|
||||
}
|
||||
|
||||
private func updateZoom(scale: Double) {
|
||||
let current = mapView.region
|
||||
let span = MKCoordinateSpan(
|
||||
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||
)
|
||||
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// 位置上报地图容器,真机封装高德 `MAMapView`。
|
||||
final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
@ -381,3 +490,4 @@ final class LocationReportMapView: UIView, MAMapViewDelegate {
|
||||
shouldCenterOnNextUserLocation = false
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@ -3,11 +3,16 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Kingfisher
|
||||
import Photos
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
import MapKit
|
||||
#endif
|
||||
|
||||
/// 打卡点详情页,对齐 Android `PunchPointDetailScreen`。
|
||||
final class PunchPointDetailViewController: BaseViewController {
|
||||
private let viewModel: PunchPointDetailViewModel
|
||||
@ -447,7 +452,192 @@ final class PunchPointDetailViewController: BaseViewController {
|
||||
}
|
||||
|
||||
/// 高德地图容器,供打卡点详情与表单复用。
|
||||
/// 打卡点地图视图,封装高德地图 marker、缩放、定位与点击选点行为。
|
||||
/// 打卡点地图视图,模拟器使用 MapKit 提供 marker、缩放、定位与点击选点行为。
|
||||
#if targetEnvironment(simulator)
|
||||
final class PunchPointMapView: UIView, MKMapViewDelegate {
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
private let mapView: MKMapView
|
||||
private var punchPointAnnotation: MKPointAnnotation?
|
||||
private var selectedAnnotation: MKPointAnnotation?
|
||||
private var punchPointImageURL: URL?
|
||||
private var fitWithUserLocation = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
mapView = MKMapView(frame: .zero)
|
||||
super.init(frame: frame)
|
||||
mapView.delegate = self
|
||||
mapView.showsUserLocation = true
|
||||
mapView.showsCompass = false
|
||||
mapView.showsScale = false
|
||||
mapView.pointOfInterestFilter = .includingAll
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(mapTapped(_:)))
|
||||
tapGesture.cancelsTouchesInView = false
|
||||
mapView.addGestureRecognizer(tapGesture)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
/// 更新原打卡点标记,可使用首张图片作为圆角 Marker。
|
||||
func updatePunchPointMarker(
|
||||
coordinate: CLLocationCoordinate2D?,
|
||||
imageURL: String? = nil,
|
||||
fitWithUserLocation: Bool = false
|
||||
) {
|
||||
if let punchPointAnnotation {
|
||||
mapView.removeAnnotation(punchPointAnnotation)
|
||||
self.punchPointAnnotation = nil
|
||||
}
|
||||
punchPointImageURL = imageURL.flatMap(URL.init(string:))
|
||||
self.fitWithUserLocation = fitWithUserLocation
|
||||
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
annotation.title = "punch_point_original"
|
||||
punchPointAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
if fitWithUserLocation {
|
||||
fitPunchPointAndUserLocation()
|
||||
}
|
||||
}
|
||||
|
||||
/// 兼容仅需更新普通 Marker 的调用。
|
||||
func updateMarker(coordinate: CLLocationCoordinate2D?) {
|
||||
updatePunchPointMarker(coordinate: coordinate)
|
||||
}
|
||||
|
||||
/// 更新地图点击产生的新位置 Marker。
|
||||
func updateSelectedMarker(coordinate: CLLocationCoordinate2D?) {
|
||||
if let selectedAnnotation {
|
||||
mapView.removeAnnotation(selectedAnnotation)
|
||||
self.selectedAnnotation = nil
|
||||
}
|
||||
guard let coordinate, CLLocationCoordinate2DIsValid(coordinate) else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
annotation.title = "punch_point_selected"
|
||||
selectedAnnotation = annotation
|
||||
mapView.addAnnotation(annotation)
|
||||
}
|
||||
|
||||
/// 设置地图中心点。
|
||||
func setCenter(_ coordinate: CLLocationCoordinate2D, zoomLevel: Double = 15, animated: Bool = true) {
|
||||
let delta = min(max(0.012 * pow(2, 15 - zoomLevel), 0.0005), 120)
|
||||
let region = MKCoordinateRegion(
|
||||
center: coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: delta, longitudeDelta: delta)
|
||||
)
|
||||
mapView.setRegion(region, animated: animated)
|
||||
}
|
||||
|
||||
/// 放大地图。
|
||||
func zoomIn() {
|
||||
updateZoom(scale: 0.5)
|
||||
}
|
||||
|
||||
/// 缩小地图。
|
||||
func zoomOut() {
|
||||
updateZoom(scale: 2)
|
||||
}
|
||||
|
||||
/// 居中到用户当前位置。
|
||||
func centerOnUserLocation() {
|
||||
guard let coordinate = mapView.userLocation.location?.coordinate,
|
||||
CLLocationCoordinate2DIsValid(coordinate),
|
||||
coordinate.latitude != 0 || coordinate.longitude != 0 else { return }
|
||||
setCenter(coordinate)
|
||||
}
|
||||
|
||||
/// 同时展示原打卡点、用户位置和新选位置。
|
||||
func fitAllMarkers(edgePadding: UIEdgeInsets = UIEdgeInsets(top: 80, left: 56, bottom: 80, right: 56)) {
|
||||
var annotations: [any MKAnnotation] = []
|
||||
if let punchPointAnnotation { annotations.append(punchPointAnnotation) }
|
||||
if let selectedAnnotation { annotations.append(selectedAnnotation) }
|
||||
if mapView.userLocation.location != nil { annotations.append(mapView.userLocation) }
|
||||
guard !annotations.isEmpty else { return }
|
||||
if annotations.count == 1, let annotation = annotations.first {
|
||||
setCenter(annotation.coordinate, zoomLevel: 16)
|
||||
} else {
|
||||
var mapRect = MKMapRect.null
|
||||
for annotation in annotations {
|
||||
let point = MKMapPoint(annotation.coordinate)
|
||||
mapRect = mapRect.union(MKMapRect(
|
||||
origin: point,
|
||||
size: MKMapSize(width: 1, height: 1)
|
||||
))
|
||||
}
|
||||
mapView.setVisibleMapRect(mapRect, edgePadding: edgePadding, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, viewFor annotation: any MKAnnotation) -> MKAnnotationView? {
|
||||
if annotation is MKUserLocation { return nil }
|
||||
let isOriginal = (annotation as AnyObject) === punchPointAnnotation
|
||||
let reuseIdentifier = isOriginal ? "PunchPointImageMarker" : "PunchPointSelectedMarker"
|
||||
let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: reuseIdentifier)
|
||||
?? MKAnnotationView(annotation: annotation, reuseIdentifier: reuseIdentifier)
|
||||
annotationView.annotation = annotation
|
||||
annotationView.canShowCallout = false
|
||||
annotationView.centerOffset = CGPoint(x: 0, y: -12)
|
||||
|
||||
if isOriginal, let punchPointImageURL {
|
||||
annotationView.image = UIImage(named: "punch_point_marker")
|
||||
KingfisherManager.shared.retrieveImage(with: punchPointImageURL) { result in
|
||||
guard case let .success(value) = result else { return }
|
||||
Task { @MainActor in
|
||||
guard annotationView.annotation === annotation else { return }
|
||||
annotationView.image = Self.roundedMarkerImage(value.image)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
annotationView.image = UIImage(named: "punch_point_marker")
|
||||
}
|
||||
return annotationView
|
||||
}
|
||||
|
||||
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
|
||||
guard userLocation.location != nil else { return }
|
||||
fitPunchPointAndUserLocation()
|
||||
}
|
||||
|
||||
private func fitPunchPointAndUserLocation() {
|
||||
guard fitWithUserLocation else { return }
|
||||
fitAllMarkers(edgePadding: UIEdgeInsets(top: 80, left: 50, bottom: 80, right: 50))
|
||||
}
|
||||
|
||||
private static func roundedMarkerImage(_ source: UIImage) -> UIImage {
|
||||
let size = CGSize(width: 40, height: 40)
|
||||
return UIGraphicsImageRenderer(size: size).image { _ in
|
||||
UIBezierPath(roundedRect: CGRect(origin: .zero, size: size), cornerRadius: 6).addClip()
|
||||
source.draw(in: CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func mapTapped(_ gesture: UITapGestureRecognizer) {
|
||||
guard gesture.state == .ended else { return }
|
||||
let point = gesture.location(in: mapView)
|
||||
onMapTap?(mapView.convert(point, toCoordinateFrom: mapView))
|
||||
}
|
||||
|
||||
private func updateZoom(scale: Double) {
|
||||
let current = mapView.region
|
||||
let span = MKCoordinateSpan(
|
||||
latitudeDelta: min(max(current.span.latitudeDelta * scale, 0.0005), 120),
|
||||
longitudeDelta: min(max(current.span.longitudeDelta * scale, 0.0005), 120)
|
||||
)
|
||||
mapView.setRegion(MKCoordinateRegion(center: current.center, span: span), animated: true)
|
||||
}
|
||||
}
|
||||
#else
|
||||
/// 打卡点地图视图,真机封装高德地图 marker、缩放、定位与点击选点行为。
|
||||
final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||||
var onMapTap: ((CLLocationCoordinate2D) -> Void)?
|
||||
|
||||
@ -614,6 +804,7 @@ final class PunchPointMapView: UIView, MAMapViewDelegate {
|
||||
onMapTap?(poi.coordinate)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// 打卡点地图控制按钮。
|
||||
/// 地图悬浮控制按钮。
|
||||
|
||||
@ -4,10 +4,15 @@
|
||||
//
|
||||
|
||||
import Kingfisher
|
||||
import MAMapKit
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
import MapKit
|
||||
#else
|
||||
import MAMapKit
|
||||
#endif
|
||||
|
||||
/// 我的举报列表页,支持按状态筛选并进入举报详情。
|
||||
final class WildPhotographerReportListViewController: BaseViewController {
|
||||
private let viewModel: WildPhotographerReportListViewModel
|
||||
@ -1797,6 +1802,49 @@ private final class WildReportDetailMediaThumbnailView: UIView {
|
||||
}
|
||||
|
||||
/// 举报详情页地图预览,绘制简化道路、路径和定位点。
|
||||
#if targetEnvironment(simulator)
|
||||
private final class WildReportDetailMapPreviewView: UIView {
|
||||
private let mapView: MKMapView
|
||||
|
||||
init(scenicName: String, detailAddress: String, coordinate: CLLocationCoordinate2D?) {
|
||||
mapView = MKMapView(frame: .zero)
|
||||
super.init(frame: .zero)
|
||||
backgroundColor = AppColor.pageBackgroundSoft
|
||||
layer.cornerRadius = 10
|
||||
clipsToBounds = true
|
||||
|
||||
mapView.showsCompass = false
|
||||
mapView.showsScale = false
|
||||
mapView.isScrollEnabled = false
|
||||
mapView.isZoomEnabled = false
|
||||
mapView.isRotateEnabled = false
|
||||
mapView.isPitchEnabled = false
|
||||
addSubview(mapView)
|
||||
mapView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
guard let coordinate else { return }
|
||||
let annotation = MKPointAnnotation()
|
||||
annotation.coordinate = coordinate
|
||||
annotation.title = scenicName
|
||||
annotation.subtitle = detailAddress
|
||||
mapView.addAnnotation(annotation)
|
||||
mapView.setRegion(
|
||||
MKCoordinateRegion(
|
||||
center: coordinate,
|
||||
span: MKCoordinateSpan(latitudeDelta: 0.006, longitudeDelta: 0.006)
|
||||
),
|
||||
animated: false
|
||||
)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
#else
|
||||
private final class WildReportDetailMapPreviewView: UIView {
|
||||
private let mapView: MAMapView
|
||||
|
||||
@ -1834,6 +1882,7 @@ private final class WildReportDetailMapPreviewView: UIView {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
/// 举报详情页处理进度时间线单行。
|
||||
private final class WildReportDetailTimelineRowView: UIView {
|
||||
|
||||
@ -3,19 +3,19 @@
|
||||
// suixinkan
|
||||
//
|
||||
|
||||
#if __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
|
||||
#if !TARGET_OS_SIMULATOR && __has_include(<AMapFoundationKit/AMapFoundationKit.h>)
|
||||
#import <AMapFoundationKit/AMapFoundationKit.h>
|
||||
#endif
|
||||
|
||||
#if __has_include(<AMapLocationKit/AMapLocationKit.h>)
|
||||
#if !TARGET_OS_SIMULATOR && __has_include(<AMapLocationKit/AMapLocationKit.h>)
|
||||
#import <AMapLocationKit/AMapLocationKit.h>
|
||||
#endif
|
||||
|
||||
#if __has_include(<MAMapKit/MAMapKit.h>)
|
||||
#if !TARGET_OS_SIMULATOR && __has_include(<MAMapKit/MAMapKit.h>)
|
||||
#import <MAMapKit/MAMapKit.h>
|
||||
#endif
|
||||
|
||||
#if __has_include(<AMapSearchKit/AMapSearchKit.h>)
|
||||
#if !TARGET_OS_SIMULATOR && __has_include(<AMapSearchKit/AMapSearchKit.h>)
|
||||
#import <AMapSearchKit/AMapSearchKit.h>
|
||||
#endif
|
||||
|
||||
|
||||
Reference in New Issue
Block a user