mirror of
https://github.com/xweiba/location-spoofer.git
synced 2026-09-21 22:30:46 +08:00
release: PaopaoLocationSpoofer v1.0.0
- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案 - MapKit 原生地图体验,支持搜索、收藏、实时定位 - 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证) - 支持 iOS 15+,SwiftUI 构建
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
import Combine
|
||||
import CoreLocation
|
||||
import Foundation
|
||||
|
||||
struct MapSelection: Equatable {
|
||||
let coordinate: CLLocationCoordinate2D
|
||||
let source: MapSelectionSource
|
||||
let explicitName: String?
|
||||
let revision: UInt64
|
||||
|
||||
static func == (lhs: MapSelection, rhs: MapSelection) -> Bool {
|
||||
lhs.coordinate.isApproximatelyEqual(to: rhs.coordinate)
|
||||
&& lhs.source == rhs.source
|
||||
&& lhs.explicitName == rhs.explicitName
|
||||
&& lhs.revision == rhs.revision
|
||||
}
|
||||
}
|
||||
|
||||
enum MapSelectionSource: Equatable {
|
||||
case initial
|
||||
case userPan
|
||||
case mapTap
|
||||
case realtime
|
||||
case search
|
||||
case favorite(UUID)
|
||||
}
|
||||
|
||||
struct RealtimeLocationIntent: Equatable {
|
||||
let id: UInt64
|
||||
let selectionRevision: UInt64
|
||||
}
|
||||
|
||||
struct MapCameraCommand: Equatable, Identifiable {
|
||||
enum Kind: Equatable {
|
||||
case focus(coordinate: CLLocationCoordinate2D, distanceMeters: CLLocationDistance)
|
||||
case zoom(factor: Double)
|
||||
|
||||
var isZoom: Bool { if case .zoom = self { return true }; return false }
|
||||
|
||||
static func == (lhs: Kind, rhs: Kind) -> Bool {
|
||||
switch (lhs, rhs) {
|
||||
case let (.focus(lhsCoordinate, lhsDistance), .focus(rhsCoordinate, rhsDistance)):
|
||||
return lhsCoordinate.isApproximatelyEqual(to: rhsCoordinate)
|
||||
&& lhsDistance == rhsDistance
|
||||
case let (.zoom(lhsFactor), .zoom(rhsFactor)):
|
||||
return lhsFactor == rhsFactor
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let id: UInt64
|
||||
let kind: Kind
|
||||
}
|
||||
|
||||
struct MapPlaceDescriptor: Equatable {
|
||||
var pointOfInterest: String?
|
||||
var streetAddress: String?
|
||||
var road: String?
|
||||
var neighborhood: String?
|
||||
var district: String?
|
||||
var city: String?
|
||||
var province: String?
|
||||
var country: String?
|
||||
|
||||
init(
|
||||
pointOfInterest: String? = nil,
|
||||
streetAddress: String? = nil,
|
||||
road: String? = nil,
|
||||
neighborhood: String? = nil,
|
||||
district: String? = nil,
|
||||
city: String? = nil,
|
||||
province: String? = nil,
|
||||
country: String? = nil
|
||||
) {
|
||||
self.pointOfInterest = pointOfInterest?.nonEmpty
|
||||
self.streetAddress = streetAddress?.nonEmpty
|
||||
self.road = road?.nonEmpty
|
||||
self.neighborhood = neighborhood?.nonEmpty
|
||||
self.district = district?.nonEmpty
|
||||
self.city = city?.nonEmpty
|
||||
self.province = province?.nonEmpty
|
||||
self.country = country?.nonEmpty
|
||||
}
|
||||
|
||||
func displayName(viewportMeters: CLLocationDistance) -> String? {
|
||||
switch viewportMeters {
|
||||
case ..<1_000:
|
||||
return firstAvailable(pointOfInterest, streetAddress, road, neighborhood, district, districtCity, cityProvince, country)
|
||||
case ..<5_000:
|
||||
return firstAvailable(road, streetAddress, neighborhood, district, districtCity, cityProvince, country)
|
||||
case ..<12_000:
|
||||
return firstAvailable(neighborhood, district, districtCity, road, city, cityProvince, country)
|
||||
case ..<100_000:
|
||||
return firstAvailable(districtCity, city, cityProvince, province, country, neighborhoodCity)
|
||||
default:
|
||||
return firstAvailable(cityProvince, provinceCountry, city, province, country, districtCity)
|
||||
}
|
||||
}
|
||||
|
||||
private var districtCity: String? { joinedDistinct(district, city) }
|
||||
private var neighborhoodCity: String? { joinedDistinct(neighborhood, city) }
|
||||
private var cityProvince: String? { joinedDistinct(city, province) }
|
||||
private var provinceCountry: String? { joinedDistinct(province, country) }
|
||||
|
||||
private func firstAvailable(_ values: String?...) -> String? {
|
||||
values.compactMap { $0?.nonEmpty }.first
|
||||
}
|
||||
|
||||
private func joinedDistinct(_ first: String?, _ second: String?) -> String? {
|
||||
let values = [first?.nonEmpty, second?.nonEmpty].compactMap { $0 }
|
||||
let unique = values.reduce(into: [String]()) { result, value in
|
||||
if !result.contains(value) { result.append(value) }
|
||||
}
|
||||
return unique.isEmpty ? nil : unique.joined(separator: " · ")
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
final class MapLocationState: ObservableObject {
|
||||
@Published private(set) var selection: MapSelection
|
||||
@Published private(set) var realtimeCoordinate: CLLocationCoordinate2D?
|
||||
@Published private(set) var realtimeLocation: CLLocation?
|
||||
@Published private(set) var cameraCommand: MapCameraCommand?
|
||||
@Published private(set) var viewportMeters: CLLocationDistance
|
||||
@Published private(set) var placeDescriptor: MapPlaceDescriptor?
|
||||
|
||||
private var nextSelectionRevision: UInt64 = 0
|
||||
private var nextCameraCommandID: UInt64 = 0
|
||||
private var nextRealtimeIntentID: UInt64 = 0
|
||||
private var latestRealtimeIntentID: UInt64 = 0
|
||||
|
||||
init(initialCoordinate: CLLocationCoordinate2D, initialViewportMeters: CLLocationDistance = 1_000) {
|
||||
viewportMeters = initialViewportMeters
|
||||
selection = MapSelection(
|
||||
coordinate: initialCoordinate,
|
||||
source: .initial,
|
||||
explicitName: nil,
|
||||
revision: nextSelectionRevision
|
||||
)
|
||||
}
|
||||
|
||||
var displayName: String? {
|
||||
selection.explicitName?.nonEmpty ?? placeDescriptor?.displayName(viewportMeters: viewportMeters)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectUserMapCenter(_ coordinate: CLLocationCoordinate2D) -> UInt64 {
|
||||
guard !selection.coordinate.isApproximatelyEqual(to: coordinate) else {
|
||||
return selection.revision
|
||||
}
|
||||
return replaceSelection(coordinate: coordinate, source: .userPan, explicitName: nil, focusDistance: nil)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectMapTap(_ coordinate: CLLocationCoordinate2D) -> UInt64 {
|
||||
replaceSelection(coordinate: coordinate, source: .mapTap, explicitName: nil, focusDistance: viewportMeters)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectSearchResult(
|
||||
_ coordinate: CLLocationCoordinate2D,
|
||||
name: String
|
||||
) -> UInt64 {
|
||||
replaceSelection(coordinate: coordinate, source: .search, explicitName: name, focusDistance: viewportMeters)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func selectFavorite(
|
||||
_ coordinate: CLLocationCoordinate2D,
|
||||
id: UUID,
|
||||
name: String
|
||||
) -> UInt64 {
|
||||
replaceSelection(coordinate: coordinate, source: .favorite(id), explicitName: name, focusDistance: viewportMeters)
|
||||
}
|
||||
|
||||
func beginRealtimeIntent() -> RealtimeLocationIntent {
|
||||
nextRealtimeIntentID &+= 1
|
||||
latestRealtimeIntentID = nextRealtimeIntentID
|
||||
return RealtimeLocationIntent(id: nextRealtimeIntentID, selectionRevision: selection.revision)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func acceptRealtimeLocation(
|
||||
_ coordinate: CLLocationCoordinate2D,
|
||||
intent: RealtimeLocationIntent
|
||||
) -> Bool {
|
||||
realtimeCoordinate = coordinate
|
||||
guard intent.id == latestRealtimeIntentID,
|
||||
intent.selectionRevision == selection.revision else {
|
||||
return false
|
||||
}
|
||||
_ = replaceSelection(
|
||||
coordinate: coordinate,
|
||||
source: .realtime,
|
||||
explicitName: nil,
|
||||
focusDistance: nil
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
func updateRealtimeLocation(_ location: CLLocation?) {
|
||||
guard let location else {
|
||||
realtimeLocation = nil
|
||||
realtimeCoordinate = nil
|
||||
return
|
||||
}
|
||||
guard CLLocationCoordinate2DIsValid(location.coordinate), location.horizontalAccuracy >= 0 else { return }
|
||||
if let current = realtimeLocation, current.timestamp > location.timestamp { return }
|
||||
realtimeLocation = location
|
||||
realtimeCoordinate = location.coordinate
|
||||
}
|
||||
|
||||
func updateRealtimeCoordinate(_ coordinate: CLLocationCoordinate2D?) {
|
||||
realtimeCoordinate = coordinate
|
||||
if coordinate == nil { realtimeLocation = nil }
|
||||
}
|
||||
|
||||
func updateExplicitName(_ name: String, forFavoriteID favoriteID: UUID) {
|
||||
guard selection.source == .favorite(favoriteID) else { return }
|
||||
selection = MapSelection(
|
||||
coordinate: selection.coordinate,
|
||||
source: selection.source,
|
||||
explicitName: name,
|
||||
revision: selection.revision
|
||||
)
|
||||
}
|
||||
|
||||
func updateViewport(distanceMeters: CLLocationDistance) {
|
||||
viewportMeters = max(50, distanceMeters)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func acceptPlaceDescriptor(_ descriptor: MapPlaceDescriptor, selectionRevision: UInt64) -> Bool {
|
||||
guard selection.revision == selectionRevision, selection.explicitName == nil else { return false }
|
||||
placeDescriptor = descriptor
|
||||
return true
|
||||
}
|
||||
|
||||
func focusSelection(distanceMeters: CLLocationDistance = 200) {
|
||||
issueCameraCommand(.focus(coordinate: selection.coordinate, distanceMeters: distanceMeters))
|
||||
}
|
||||
|
||||
func zoom(by factor: Double) {
|
||||
guard factor.isFinite, factor > 0 else { return }
|
||||
issueCameraCommand(.zoom(factor: factor))
|
||||
}
|
||||
|
||||
private func replaceSelection(
|
||||
coordinate: CLLocationCoordinate2D,
|
||||
source: MapSelectionSource,
|
||||
explicitName: String?,
|
||||
focusDistance: CLLocationDistance?
|
||||
) -> UInt64 {
|
||||
nextSelectionRevision &+= 1
|
||||
placeDescriptor = nil
|
||||
selection = MapSelection(
|
||||
coordinate: coordinate,
|
||||
source: source,
|
||||
explicitName: explicitName?.nonEmpty,
|
||||
revision: nextSelectionRevision
|
||||
)
|
||||
if let focusDistance {
|
||||
issueCameraCommand(.focus(coordinate: coordinate, distanceMeters: focusDistance))
|
||||
}
|
||||
return nextSelectionRevision
|
||||
}
|
||||
|
||||
private func issueCameraCommand(_ kind: MapCameraCommand.Kind) {
|
||||
nextCameraCommandID &+= 1
|
||||
cameraCommand = MapCameraCommand(id: nextCameraCommandID, kind: kind)
|
||||
}
|
||||
}
|
||||
|
||||
private extension String {
|
||||
var nonEmpty: String? {
|
||||
let value = trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return value.isEmpty ? nil : value
|
||||
}
|
||||
}
|
||||
|
||||
extension CLLocationCoordinate2D {
|
||||
func isApproximatelyEqual(to other: CLLocationCoordinate2D, tolerance: CLLocationDegrees = 0.000_001) -> Bool {
|
||||
abs(latitude - other.latitude) < tolerance && abs(longitude - other.longitude) < tolerance
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 持久化存储
|
||||
|
||||
enum ViewportStore {
|
||||
private static let key = "mapViewportMeters"
|
||||
static func save(_ meters: CLLocationDistance) {
|
||||
UserDefaults.standard.set(meters, forKey: key)
|
||||
}
|
||||
/// 取持久化缩放值;未存过返回 nil
|
||||
static func load() -> CLLocationDistance? {
|
||||
let v = UserDefaults.standard.double(forKey: key)
|
||||
return v > 0 ? v : nil
|
||||
}
|
||||
/// 取持久化缩放值,取不到返回默认 1km 并立即存储
|
||||
static func loadOrDefault() -> CLLocationDistance {
|
||||
if let v = load() { return v }
|
||||
let fallback: CLLocationDistance = 1_000
|
||||
save(fallback)
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
struct LastCoordinate {
|
||||
let latitude: Double
|
||||
let longitude: Double
|
||||
var coordinate: CLLocationCoordinate2D { CLLocationCoordinate2D(latitude: latitude, longitude: longitude) }
|
||||
var isValid: Bool { CLLocationCoordinate2DIsValid(coordinate) && (latitude != 0 || longitude != 0) }
|
||||
}
|
||||
|
||||
enum LastCoordinateStore {
|
||||
private static let latKey = "lastMapLat"
|
||||
private static let lonKey = "lastMapLon"
|
||||
static func save(lat: Double, lon: Double) {
|
||||
UserDefaults.standard.set(lat, forKey: latKey)
|
||||
UserDefaults.standard.set(lon, forKey: lonKey)
|
||||
}
|
||||
/// 取持久化坐标,未存过或无效返回 nil
|
||||
static func load() -> LastCoordinate? {
|
||||
let c = LastCoordinate(
|
||||
latitude: UserDefaults.standard.double(forKey: latKey),
|
||||
longitude: UserDefaults.standard.double(forKey: lonKey)
|
||||
)
|
||||
return c.isValid ? c : nil
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user