Files
location-spoofer/App/MapViewRepresentable.swift
T
xweiba c8aba2be78 release: PaopaoLocationSpoofer v1.0.0
- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案
- MapKit 原生地图体验,支持搜索、收藏、实时定位
- 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证)
- 支持 iOS 15+,SwiftUI 构建
2026-08-05 15:45:24 +08:00

179 lines
7.1 KiB
Swift

import CoreLocation
import Foundation
import MapKit
import SwiftUI
import UIKit
enum MapZoomMath {
private static let minimumDelta = 0.000_05
private static let maximumLatitudeDelta = 170.0
private static let maximumLongitudeDelta = 360.0
static func scaledSpan(_ span: MKCoordinateSpan, factor: Double) -> MKCoordinateSpan {
guard factor.isFinite, factor > 0 else { return span }
return MKCoordinateSpan(
latitudeDelta: min(max(span.latitudeDelta * factor, minimumDelta), maximumLatitudeDelta),
longitudeDelta: min(max(span.longitudeDelta * factor, minimumDelta), maximumLongitudeDelta)
)
}
static func viewportScaleLabel(distanceMeters: CLLocationDistance) -> String {
let meters = max(0, distanceMeters)
if meters < 1_000 {
return "\(Int(meters.rounded())) m"
}
let kilometers = meters / 1_000
if kilometers < 10 {
return String(format: "%.1f km", kilometers)
}
return "\(Int(kilometers.rounded())) km"
}
}
struct MapViewRepresentable: UIViewRepresentable {
let selection: MapSelection
let cameraCommand: MapCameraCommand?
let onRealtimeLocationChanged: (CLLocation) -> Void
let onUserCenterChanged: (CLLocationCoordinate2D, CLLocationDistance) -> Void
let onViewportChanged: (CLLocationDistance) -> Void
let onMapTap: (CLLocationCoordinate2D) -> Void
let onUserZoomChanged: ((CLLocationDistance) -> Void)?
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
map.showsUserLocation = true
let initialDistance = ViewportStore.loadOrDefault()
map.setRegion(
MKCoordinateRegion(
center: selection.coordinate,
latitudinalMeters: initialDistance,
longitudinalMeters: initialDistance
),
animated: false
)
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
tap.cancelsTouchesInView = false
map.addGestureRecognizer(tap)
context.coordinator.map = map
return map
}
func updateUIView(_ map: MKMapView, context: Context) {
context.coordinator.parent = self
context.coordinator.consume(cameraCommand, on: map)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewRepresentable
weak var map: MKMapView?
private var lastConsumedCommandID: UInt64?
private var activeCameraCommandID: UInt64?
private var activeCommandIsZoom = false
private var regionChangeWasUserDriven = false
private var isPinchZoom = false
init(parent: MapViewRepresentable) {
self.parent = parent
}
func consume(_ command: MapCameraCommand?, on map: MKMapView) {
guard let command, command.id != lastConsumedCommandID else { return }
lastConsumedCommandID = command.id
activeCameraCommandID = command.id
activeCommandIsZoom = command.kind.isZoom
regionChangeWasUserDriven = false
map.userTrackingMode = .none
let region: MKCoordinateRegion
switch command.kind {
case let .focus(coordinate, _):
// 保持当前 span 不变,只移动中心点,避免 latitudinalMeters
// 与 visibleVerticalDistance 之间因屏幕宽高比引入 2x 漂移
region = MKCoordinateRegion(center: coordinate, span: map.region.span)
case let .zoom(factor):
region = MKCoordinateRegion(
center: map.centerCoordinate,
span: MapZoomMath.scaledSpan(map.region.span, factor: factor)
)
}
map.setRegion(region, animated: true)
}
@objc func handleTap(_ gesture: UITapGestureRecognizer) {
guard gesture.state == .ended, let map else { return }
let point = gesture.location(in: map)
parent.onMapTap(map.convert(point, toCoordinateFrom: map))
}
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
guard let location = userLocation.location,
CLLocationCoordinate2DIsValid(location.coordinate),
location.horizontalAccuracy >= 0 else { return }
parent.onRealtimeLocationChanged(location)
}
func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) {
let activeRecognizers = ([mapView as UIView] + mapView.subviews)
.compactMap(\.gestureRecognizers)
.flatMap { $0 }
.filter { recognizer in
recognizer.state == .began || recognizer.state == .changed
}
let hasActiveGesture = !activeRecognizers.isEmpty
// Pinching updates the viewport/name granularity only. MapKit can
// keep its pan recognizer active during a pinch, so only a pure pan
// is allowed to replace the selected center coordinate.
let hasActivePan = activeRecognizers.contains { $0 is UIPanGestureRecognizer }
let hasActivePinch = activeRecognizers.contains { $0 is UIPinchGestureRecognizer }
regionChangeWasUserDriven = hasActivePan && !hasActivePinch
isPinchZoom = hasActivePinch
if hasActiveGesture {
activeCameraCommandID = nil
activeCommandIsZoom = false
}
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let distance = visibleVerticalDistance(in: mapView)
parent.onViewportChanged(distance)
let userZoomed: Bool
if activeCameraCommandID != nil {
userZoomed = activeCommandIsZoom
activeCameraCommandID = nil
activeCommandIsZoom = false
} else if isPinchZoom {
userZoomed = true
isPinchZoom = false
} else {
userZoomed = false
}
if userZoomed {
parent.onUserZoomChanged?(distance)
} else if regionChangeWasUserDriven {
parent.onUserCenterChanged(mapView.centerCoordinate, distance)
}
regionChangeWasUserDriven = false
}
private func visibleVerticalDistance(in map: MKMapView) -> CLLocationDistance {
let centerX = map.bounds.midX
let north = map.convert(CGPoint(x: centerX, y: map.bounds.minY), toCoordinateFrom: map)
let south = map.convert(CGPoint(x: centerX, y: map.bounds.maxY), toCoordinateFrom: map)
let northLocation = CLLocation(latitude: north.latitude, longitude: north.longitude)
let southLocation = CLLocation(latitude: south.latitude, longitude: south.longitude)
let measured = northLocation.distance(from: southLocation)
return measured.isFinite && measured > 0 ? measured : 1_000
}
}
}