mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-23 15:11:54 +08:00
release: PaopaoLocationSpoofer v1.0.0
- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案 - MapKit 原生地图体验,支持搜索、收藏、实时定位 - 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证) - 支持 iOS 15+,SwiftUI 构建
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user