fix: 坐标转换修复、图钉键盘对齐、WiFi监控、UI优化

- 坐标转换: detectType 用地理边界判断瓦片类型,加载路径去掉重复 toStored
- 图钉: 放 MKMapView 内部 + keyboard 通知双保险,针尖对齐地理中心
- 缩放控件: 转 UIKit 子视图放地图内,键盘弹起一起移动
- 网络监控: WiFi SSID 轮询 + 重连检测,虚拟定位激活时自动验证
- UI: 去飞行模式弹窗和面板,引导页只在首次安装和设置入口触发
- 实时定位: 蓝点数据优先,不限时缓存
This commit is contained in:
xweiba
2026-08-05 21:20:56 +08:00
parent c8aba2be78
commit 4aa4f63e62
17 changed files with 353 additions and 211 deletions
+1 -38
View File
@@ -4,7 +4,6 @@ struct ContentView: View {
@StateObject private var setup = SetupCoordinator()
@ObservedObject private var net = NetworkMonitor.shared
@State private var showSetup = false
@State private var showEnableTip = false
@AppStorage("setupCompleted") private var setupCompleted = false
var body: some View {
@@ -12,7 +11,6 @@ struct ContentView: View {
MapHomeView(setup: setup)
}
.task {
//
if !setupCompleted {
showSetup = true
return
@@ -21,10 +19,7 @@ struct ContentView: View {
}
.onChange(of: net.isAirplaneMode) { airplane in
guard setupCompleted else { return }
if airplane {
showEnableTip = true
} else {
showEnableTip = false
if !airplane {
Task { await setup.refreshTrust() }
}
}
@@ -35,37 +30,5 @@ struct ContentView: View {
showSetup = false
})
}
//
.onChange(of: setup.needsSetup) { needs in
if needs { showSetup = true }
}
.sheet(isPresented: $showEnableTip) {
NavigationView {
VStack(spacing: 20) {
Image(systemName: "airplane")
.font(.system(size: 48))
.foregroundStyle(.orange)
Text("飞行模式已开启")
.font(.title3.weight(.semibold))
Text("Wi‑Fi 和蜂窝数据已关闭,虚拟定位无法生效。请关闭飞行模式后重试。")
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
Button("知道了") {
showEnableTip = false
}
.buttonStyle(.borderedProminent)
.frame(maxWidth: .infinity)
}
.padding(30)
.navigationTitle("提示")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("完成") { showEnableTip = false }
}
}
}
}
}
}
+6 -3
View File
@@ -56,9 +56,8 @@ final class LocationActionCoordinator: ObservableObject {
}
private func commit(_ favorite: FavoriteLocation) -> Bool {
// MKMapView 使GCJ-02 GCJ-02
// Apple wloc 使 WGS-84 WGS-84
let wgs = CoordinateConverter.gcj02ToWgs84(lat: favorite.latitude, lon: favorite.longitude)
// WGS-84
let wgs = CoordinateConverter.toStored(lat: favorite.latitude, lon: favorite.longitude)
WlocSettingsStore.save(WlocSettings(
longitude: wgs.lon,
latitude: wgs.lat,
@@ -71,6 +70,10 @@ final class LocationActionCoordinator: ObservableObject {
enabled: true,
accuracy: favorite.accuracy
)
RuntimeLogger.info("APP", "坐标转换", "写入代理: WGS-84", details: [
"lat": String(wgs.lat),
"lon": String(wgs.lon)
])
state = .idle
virtualLocationEnabled = true
message = "虚拟定位已开启"
+59 -90
View File
@@ -66,10 +66,11 @@ struct MapHomeView: View {
self.setup = setup
let savedCoord = LastCoordinateStore.load()
let initialZoom = ViewportStore.loadOrDefault()
// initializeMap
// WGS-84
let initialCoord: CLLocationCoordinate2D
if let coord = savedCoord?.coordinate {
initialCoord = coord
let display = CoordinateConverter.toDisplay(lat: coord.latitude, lon: coord.longitude)
initialCoord = CLLocationCoordinate2D(latitude: display.lat, longitude: display.lon)
} else {
initialCoord = CLLocationCoordinate2D(latitude: 22.544577, longitude: 113.94114)
}
@@ -89,10 +90,12 @@ struct MapHomeView: View {
},
onUserCenterChanged: { coordinate, distance in
mapState.updateViewport(distanceMeters: distance)
CoordinateConverter.updateTileType(lat: coordinate.latitude, lon: coordinate.longitude)
let previousRevision = mapState.selection.revision
let revision = mapState.selectUserMapCenter(coordinate)
guard revision != previousRevision else { return }
LastCoordinateStore.save(lat: coordinate.latitude, lon: coordinate.longitude)
let wgs = CoordinateConverter.toStored(lat: coordinate.latitude, lon: coordinate.longitude)
LastCoordinateStore.save(lat: wgs.lat, lon: wgs.lon)
favorites.select(nil)
scheduleGeocode(coordinate: coordinate, revision: revision)
},
@@ -101,35 +104,19 @@ struct MapHomeView: View {
},
onMapTap: { coordinate in
favorites.select(nil)
CoordinateConverter.updateTileType(lat: coordinate.latitude, lon: coordinate.longitude)
let revision = mapState.selectMapTap(coordinate)
LastCoordinateStore.save(lat: coordinate.latitude, lon: coordinate.longitude)
let wgs = CoordinateConverter.toStored(lat: coordinate.latitude, lon: coordinate.longitude)
LastCoordinateStore.save(lat: wgs.lat, lon: wgs.lon)
scheduleGeocode(coordinate: coordinate, revision: revision)
},
onUserZoomChanged: { distance in
ViewportStore.save(distance)
}
},
onZoomIn: { mapState.zoom(by: 0.5) },
onZoomOut: { mapState.zoom(by: 2) }
)
.ignoresSafeArea()
.ignoresSafeArea(.keyboard)
.overlay {
Image(systemName: "mappin.and.ellipse")
.font(.system(size: 38, weight: .semibold))
.symbolRenderingMode(.palette)
.foregroundStyle(.white, .red)
.shadow(color: .black.opacity(0.28), radius: 5, y: 3)
.offset(y: -19)
.allowsHitTesting(false)
.accessibilityHidden(true)
}
HStack {
zoomControls
Spacer()
}
.padding(.leading, 16)
.padding(.top, 130)
.allowsHitTesting(true)
.allowsHitTesting(true)
.ignoresSafeArea(.container)
VStack(spacing: 10) {
topControls
@@ -195,12 +182,21 @@ struct MapHomeView: View {
)) {
Button("知道了", role: .cancel) {}
} message: { Text(manualHint) }
.onAppear(perform: initializeMap)
.onAppear {
initializeMap()
NetworkMonitor.shared.onWiFiChanged = { [self] in
guard spoofState == .active else { return }
Task { @MainActor in
let target = currentSelectionFavorite
let result = await setup.runVerificationTest(testLat: target.latitude, testLon: target.longitude)
if !result.isSuccess, let tip = result.tipKind {
activeTip = tip
}
}
}
}
.onChange(of: net.isAirplaneMode) { airplane in
if airplane {
showEnableTip = true
} else {
showEnableTip = false
if !airplane {
Task { await setup.refreshTrust() }
}
}
@@ -362,14 +358,16 @@ struct MapHomeView: View {
return
}
let snapshot = currentSelectionFavorite
let wgs = CoordinateConverter.toStored(lat: snapshot.latitude, lon: snapshot.longitude)
let favorite = favorites.save(
name: snapshot.name,
latitude: snapshot.latitude,
longitude: snapshot.longitude,
latitude: wgs.lat,
longitude: wgs.lon,
accuracy: snapshot.accuracy
)
let display = CoordinateConverter.toDisplay(lat: favorite.latitude, lon: favorite.longitude)
mapState.selectFavorite(
CLLocationCoordinate2D(latitude: favorite.latitude, longitude: favorite.longitude),
CLLocationCoordinate2D(latitude: display.lat, longitude: display.lon),
id: favorite.id,
name: favorite.name
)
@@ -391,15 +389,6 @@ struct MapHomeView: View {
HStack(spacing: 8) { ForEach(favorites.favorites) { f in favoriteChip(f) } }.padding(.vertical, 2)
}
}
//
if net.isAirplaneMode {
HStack {
Image(systemName: "airplane").foregroundStyle(.orange)
Text("飞行模式已开启").font(.caption).foregroundStyle(.orange)
Spacer()
Button("查看说明") { activeTip = .activation }.font(.caption).buttonStyle(.bordered).tint(.orange)
}
}
//
HStack(spacing: 10) {
Button(action: handleMainButtonTap) {
@@ -504,12 +493,24 @@ struct MapHomeView: View {
activeSpoofLat = target.latitude
activeSpoofLon = target.longitude
}
RuntimeLogger.info("APP", "定位", "验证结果", details: [
"success": "true",
"applied": String(applied),
"spoofState": String(describing: spoofState)
])
if applied && !activationTipDisabled {
showEnableTip = true
activationTipCount += 1
}
} else {
spoofState = actions.virtualLocationEnabled ? .active : .idle
RuntimeLogger.warning("APP", "定位", "验证失败", details: [
"result": result.id,
"spoofState": String(describing: spoofState)
])
if let tip = result.tipKind {
activeTip = tip
}
}
locationOperationTask = nil
}
@@ -572,51 +573,14 @@ struct MapHomeView: View {
private var testFavorite: FavoriteLocation { currentSelectionFavorite }
private var zoomControls: some View {
VStack(spacing: 0) {
Button {
mapState.zoom(by: 0.5)
} label: {
Image(systemName: "plus")
.font(.system(size: 22, weight: .bold))
.frame(width: 52, height: 52)
}
.accessibilityLabel("放大地图")
Divider().frame(width: 28)
Text(MapZoomMath.viewportScaleLabel(distanceMeters: mapState.viewportMeters))
.font(.system(size: 9, weight: .semibold, design: .rounded))
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.7)
.frame(width: 54, height: 28)
.accessibilityLabel("当前地图范围")
.accessibilityValue(MapZoomMath.viewportScaleLabel(distanceMeters: mapState.viewportMeters))
Divider().frame(width: 28)
Button {
mapState.zoom(by: 2)
} label: {
Image(systemName: "minus")
.font(.system(size: 22, weight: .bold))
.frame(width: 52, height: 52)
}
.accessibilityLabel("缩小地图")
}
.buttonStyle(.plain)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 13, style: .continuous))
.shadow(color: .black.opacity(0.18), radius: 7, y: 3)
}
private func initializeMap() {
guard !mapDidInitialize else { return }
mapDidInitialize = true
if let selected = favorites.selectedFavorite {
let display = CoordinateConverter.toDisplay(lat: selected.latitude, lon: selected.longitude)
mapState.selectFavorite(
CLLocationCoordinate2D(latitude: selected.latitude, longitude: selected.longitude),
CLLocationCoordinate2D(latitude: display.lat, longitude: display.lon),
id: selected.id,
name: selected.name
)
@@ -638,13 +602,13 @@ struct MapHomeView: View {
private func requestRealtimeLocation() {
let intent = mapState.beginRealtimeIntent()
if let nativeLocation = mapState.realtimeLocation,
abs(nativeLocation.timestamp.timeIntervalSinceNow) <= 30 {
acceptRealtimeLocation(nativeLocation.coordinate, intent: intent, source: "MapKit 实时位置")
// MKMapView userLocation
if let loc = mapState.realtimeLocation {
acceptRealtimeLocation(loc.coordinate, intent: intent, source: "MapKit蓝点")
return
}
startRealtimeLocationRequest(
source: "定位按钮兜底",
source: "CLLocationManager",
showFailureAlert: true,
intent: intent
)
@@ -659,7 +623,7 @@ struct MapHomeView: View {
// CLLocationManager fallback so the camera and dot cannot disagree.
realtimeRequestContext = nil
realtimeRequestTask?.cancel()
acceptRealtimeLocation(location.coordinate, intent: context.intent, source: "MapKit \(context.source)")
acceptRealtimeLocation(location.coordinate, intent: context.intent, source: "蓝点(途中)→\(context.source)")
}
private func startRealtimeLocationRequest(
@@ -711,7 +675,9 @@ struct MapHomeView: View {
guard accepted else { return }
//
mapState.focusSelection(distanceMeters: currentViewport)
LastCoordinateStore.save(lat: coordinate.latitude, lon: coordinate.longitude)
CoordinateConverter.updateTileType(lat: coordinate.latitude, lon: coordinate.longitude)
let wgs = CoordinateConverter.toStored(lat: coordinate.latitude, lon: coordinate.longitude)
LastCoordinateStore.save(lat: wgs.lat, lon: wgs.lon)
favorites.select(nil)
scheduleGeocode(coordinate: coordinate, revision: mapState.selection.revision)
}
@@ -842,8 +808,10 @@ struct MapHomeView: View {
geocodeDebounceTask?.cancel()
reverseGeocodeTask?.cancel()
favorites.select(nil)
CoordinateConverter.updateTileType(lat: result.coordinate.latitude, lon: result.coordinate.longitude)
mapState.selectSearchResult(result.coordinate, name: result.name)
LastCoordinateStore.save(lat: result.coordinate.latitude, lon: result.coordinate.longitude)
let wgs = CoordinateConverter.toStored(lat: result.coordinate.latitude, lon: result.coordinate.longitude)
LastCoordinateStore.save(lat: wgs.lat, lon: wgs.lon)
searchText = result.name
searchResults = []
searchError = ""
@@ -857,8 +825,9 @@ struct MapHomeView: View {
geocodeDebounceTask?.cancel()
reverseGeocodeTask?.cancel()
favorites.select(favorite.id)
let display = CoordinateConverter.toDisplay(lat: favorite.latitude, lon: favorite.longitude)
mapState.selectFavorite(
CLLocationCoordinate2D(latitude: favorite.latitude, longitude: favorite.longitude),
CLLocationCoordinate2D(latitude: display.lat, longitude: display.lon),
id: favorite.id,
name: favorite.name
)
+125
View File
@@ -38,6 +38,8 @@ struct MapViewRepresentable: UIViewRepresentable {
let onViewportChanged: (CLLocationDistance) -> Void
let onMapTap: (CLLocationCoordinate2D) -> Void
let onUserZoomChanged: ((CLLocationDistance) -> Void)?
var onZoomIn: (() -> Void)?
var onZoomOut: (() -> Void)?
func makeCoordinator() -> Coordinator { Coordinator(parent: self) }
@@ -55,10 +57,83 @@ struct MapViewRepresentable: UIViewRepresentable {
animated: false
)
// Center pin positioned relative to geographic center, not Auto Layout
let pinSize: CGFloat = 38
let sizeCfg = UIImage.SymbolConfiguration(pointSize: pinSize, weight: .semibold)
let paletteCfg = UIImage.SymbolConfiguration(paletteColors: [.white, .red])
let pinImage = UIImage(systemName: "mappin",
withConfiguration: sizeCfg.applying(paletteCfg))
let pin = UIImageView(image: pinImage)
pin.frame = CGRect(x: 0, y: 0, width: pinSize, height: pinSize)
pin.isUserInteractionEnabled = false
pin.layer.shadowColor = UIColor.black.cgColor
pin.layer.shadowOpacity = 0.28
pin.layer.shadowRadius = 5
pin.layer.shadowOffset = CGSize(width: 0, height: 3)
map.addSubview(pin)
context.coordinator.centerPin = pin
// Zoom controls UIKit subviews inside MKMapView, move with the map
let zoomStack = UIStackView()
zoomStack.axis = .vertical
zoomStack.spacing = 0
zoomStack.translatesAutoresizingMaskIntoConstraints = false
zoomStack.alignment = .center
zoomStack.backgroundColor = UIColor.systemBackground.withAlphaComponent(0.8)
zoomStack.layer.cornerRadius = 13
zoomStack.layer.shadowColor = UIColor.black.cgColor
zoomStack.layer.shadowOpacity = 0.18
zoomStack.layer.shadowRadius = 7
zoomStack.layer.shadowOffset = CGSize(width: 0, height: 3)
let zoomInBtn = UIButton(type: .system)
zoomInBtn.setImage(UIImage(systemName: "plus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .bold)), for: .normal)
zoomInBtn.addTarget(context.coordinator, action: #selector(Coordinator.zoomInTapped), for: .touchUpInside)
zoomInBtn.heightAnchor.constraint(equalToConstant: 52).isActive = true
zoomInBtn.widthAnchor.constraint(equalToConstant: 52).isActive = true
let zoomLabel = UILabel()
let roundedDesc = UIFont.systemFont(ofSize: 9, weight: .semibold).fontDescriptor.withDesign(.rounded)
zoomLabel.font = roundedDesc.flatMap { UIFont(descriptor: $0, size: 9) } ?? .systemFont(ofSize: 9, weight: .semibold)
zoomLabel.textColor = .secondaryLabel
zoomLabel.textAlignment = .center
zoomLabel.adjustsFontSizeToFitWidth = true
zoomLabel.minimumScaleFactor = 0.7
zoomLabel.text = MapZoomMath.viewportScaleLabel(distanceMeters: ViewportStore.loadOrDefault())
zoomLabel.heightAnchor.constraint(equalToConstant: 28).isActive = true
context.coordinator.zoomLabel = zoomLabel
let zoomOutBtn = UIButton(type: .system)
zoomOutBtn.setImage(UIImage(systemName: "minus", withConfiguration: UIImage.SymbolConfiguration(pointSize: 22, weight: .bold)), for: .normal)
zoomOutBtn.addTarget(context.coordinator, action: #selector(Coordinator.zoomOutTapped), for: .touchUpInside)
zoomOutBtn.heightAnchor.constraint(equalToConstant: 52).isActive = true
zoomOutBtn.widthAnchor.constraint(equalToConstant: 52).isActive = true
let sep1 = UIView(); sep1.translatesAutoresizingMaskIntoConstraints = false
sep1.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
sep1.backgroundColor = .separator
sep1.widthAnchor.constraint(equalToConstant: 28).isActive = true
let sep2 = UIView(); sep2.translatesAutoresizingMaskIntoConstraints = false
sep2.heightAnchor.constraint(equalToConstant: 0.5).isActive = true
sep2.backgroundColor = .separator
sep2.widthAnchor.constraint(equalToConstant: 28).isActive = true
zoomStack.addArrangedSubview(zoomInBtn)
zoomStack.addArrangedSubview(sep1)
zoomStack.addArrangedSubview(zoomLabel)
zoomStack.addArrangedSubview(sep2)
zoomStack.addArrangedSubview(zoomOutBtn)
map.addSubview(zoomStack)
NSLayoutConstraint.activate([
zoomStack.leadingAnchor.constraint(equalTo: map.safeAreaLayoutGuide.leadingAnchor, constant: 16),
zoomStack.topAnchor.constraint(equalTo: map.safeAreaLayoutGuide.topAnchor, constant: 130),
])
let tap = UITapGestureRecognizer(target: context.coordinator, action: #selector(Coordinator.handleTap(_:)))
tap.cancelsTouchesInView = false
map.addGestureRecognizer(tap)
context.coordinator.map = map
context.coordinator.setupKeyboardObservers()
return map
}
@@ -76,11 +151,41 @@ struct MapViewRepresentable: UIViewRepresentable {
private var activeCommandIsZoom = false
private var regionChangeWasUserDriven = false
private var isPinchZoom = false
weak var zoomLabel: UILabel?
weak var centerPin: UIImageView?
private let pinSize: CGFloat = 38
// MKUserLocationView 20pt
private var userDotDiameter: CGFloat = 20
@objc func zoomInTapped() { parent.onZoomIn?() }
@objc func zoomOutTapped() { parent.onZoomOut?() }
init(parent: MapViewRepresentable) {
self.parent = parent
}
private func updatePinPosition(on mapView: MKMapView) {
guard let pin = centerPin else { return }
let centerPt = mapView.convert(mapView.centerCoordinate, toPointTo: mapView)
// pin +
pin.center = CGPoint(
x: centerPt.x,
y: centerPt.y - pinSize / 2 + 5
)
}
func setupKeyboardObservers() {
let nc = NotificationCenter.default
nc.addObserver(forName: UIResponder.keyboardWillShowNotification, object: nil, queue: .main) { [weak self] _ in
guard let self, let map = self.map else { return }
self.updatePinPosition(on: map)
}
nc.addObserver(forName: UIResponder.keyboardWillHideNotification, object: nil, queue: .main) { [weak self] _ in
guard let self, let map = self.map else { return }
self.updatePinPosition(on: map)
}
}
func consume(_ command: MapCameraCommand?, on map: MKMapView) {
guard let command, command.id != lastConsumedCommandID else { return }
lastConsumedCommandID = command.id
@@ -111,6 +216,16 @@ struct MapViewRepresentable: UIViewRepresentable {
parent.onMapTap(map.convert(point, toCoordinateFrom: map))
}
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
for view in views where view.annotation is MKUserLocation {
//
userDotDiameter = view.bounds.width
for sub in view.subviews where sub.bounds.width > userDotDiameter + 4 {
sub.isHidden = true
}
}
}
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
guard let location = userLocation.location,
CLLocationCoordinate2DIsValid(location.coordinate),
@@ -143,6 +258,16 @@ struct MapViewRepresentable: UIViewRepresentable {
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
let distance = visibleVerticalDistance(in: mapView)
parent.onViewportChanged(distance)
zoomLabel?.text = MapZoomMath.viewportScaleLabel(distanceMeters: distance)
updatePinPosition(on: mapView)
//
let c = mapView.centerCoordinate
CoordinateConverter.updateTileType(lat: c.latitude, lon: c.longitude)
// delegate mapState.realtimeLocation nil
if let ul = mapView.userLocation.location,
CLLocationCoordinate2DIsValid(ul.coordinate), ul.horizontalAccuracy >= 0 {
parent.onRealtimeLocationChanged(ul)
}
let userZoomed: Bool
if activeCameraCommandID != nil {
+3
View File
@@ -25,6 +25,9 @@ final class ProxyManager: ObservableObject {
let lon = settings.flatMap { $0.enabled ? $0.longitude : nil } ?? 0
let enabled = (settings?.enabled ?? false) ? CInt(1) : CInt(0)
let accuracy = CInt(settings?.accuracy ?? 25)
RuntimeLogger.info("APP", "坐标转换", "启动代理: WGS-84", details: [
"lat": String(lat), "lon": String(lon)
])
let result: UInt = authority.certPEM.withCString { cp in
authority.keyPEM.withCString { kp in
UInt(wloccore_startproxy(UnsafeMutablePointer(mutating: cp), UnsafeMutablePointer(mutating: kp), CDouble(lat), CDouble(lon), enabled, accuracy))
+2 -9
View File
@@ -61,7 +61,7 @@ final class RealtimeLocationManager: NSObject, ObservableObject, CLLocationManag
private let driver: RealtimeLocationDriving
private let oneShotTimeoutNanoseconds: UInt64
private let fallbackTimeoutNanoseconds: UInt64
private let cacheMaxAge: TimeInterval
private let cacheMaxAge: TimeInterval = 20
private var nextRequestID: UInt64 = 0
private var activeRequest: ActiveRequest?
private var timeoutTask: Task<Void, Never>?
@@ -69,13 +69,11 @@ final class RealtimeLocationManager: NSObject, ObservableObject, CLLocationManag
init(
driver: RealtimeLocationDriving,
oneShotTimeoutNanoseconds: UInt64 = 1_500_000_000,
fallbackTimeoutNanoseconds: UInt64 = 5_000_000_000,
cacheMaxAge: TimeInterval = 20
fallbackTimeoutNanoseconds: UInt64 = 5_000_000_000
) {
self.driver = driver
self.oneShotTimeoutNanoseconds = oneShotTimeoutNanoseconds
self.fallbackTimeoutNanoseconds = fallbackTimeoutNanoseconds
self.cacheMaxAge = cacheMaxAge
authorizationStatus = driver.authorizationStatus
super.init()
driver.delegate = self
@@ -105,11 +103,6 @@ final class RealtimeLocationManager: NSObject, ObservableObject, CLLocationManag
if let cached = freshestCachedLocation() {
location = cached
RuntimeLogger.info("APP", "定位", "使用系统缓存实时定位", details: [
"age": String(format: "%.2f", max(0, -cached.timestamp.timeIntervalSinceNow)),
"lat": String(cached.coordinate.latitude),
"lon": String(cached.coordinate.longitude)
])
return cached.coordinate
}
-52
View File
@@ -54,15 +54,8 @@ final class SetupCoordinator: ObservableObject {
message = "代理链路异常,未收到响应"
return
}
// Go Core Apple
let patchResult = CoreBridge.testWlocPatch(lat: testLat, lon: testLon, accuracy: testAccuracy)
if patchResult.hasPrefix("ok:") {
trustState = .trusted
message = "✓ 定位环境正常(返回 \(status)"
} else {
trustState = .unavailable
message = "定位数据改写验证失败:\(patchResult)"
}
} catch {
trustState = .unavailable
let ns = error as NSError
@@ -107,7 +100,6 @@ final class SetupCoordinator: ObservableObject {
log("======== 代理验证测试 ========")
log("App 版本: \(appVersion)")
log("系统版本: iOS \(UIDevice.current.systemVersion)")
log("测试目标: lat=\(testLat), lon=\(testLon)")
log("")
// Step A: Proxy running
@@ -126,18 +118,6 @@ final class SetupCoordinator: ObservableObject {
}
collectProxyLogs(since: stepAStart, to: log)
// Verification must never overwrite a newer location action.
let previousCoordinates = proxy.coordinateSnapshot(
accuracy: WlocSettingsStore.load()?.accuracy ?? 25
)
var verificationCoordinateRevision: UInt64?
defer {
if let revision = verificationCoordinateRevision {
let restored = proxy.restoreCoords(previousCoordinates, ifUnchangedSince: revision)
log(restored ? " ↩ 已恢复验证前的代理坐标" : " ↩ 检测到更新位置,跳过旧坐标恢复")
}
}
// Step B: Combined CA + WiFi proxy check (single request)
log("")
log("[步骤 B] 检测证书与 WiFi 代理…")
@@ -181,38 +161,6 @@ final class SetupCoordinator: ObservableObject {
}
collectProxyLogs(since: stepBStart, to: log)
// Step C: Write and verify coordinates
log("[步骤 C] 写入测试坐标并验证…")
guard let testCoordinateRevision = proxy.setCoordsIfUnchanged(
lat: testLat,
lon: testLon,
enabled: true,
accuracy: 25,
expectedRevision: previousCoordinates.revision
) else {
log(" ↪ 检测到更新位置,取消过期验证坐标写入")
return .verificationSuperseded
}
verificationCoordinateRevision = testCoordinateRevision
let coords = proxy.getCoords()
if coords.enabled && abs(coords.lat - testLat) < 0.001 && abs(coords.lon - testLon) < 0.001 {
log(" ✓ 坐标写入成功: lat=\(coords.lat) lon=\(coords.lon)")
} else {
log(" ✗ 坐标验证失败: enabled=\(coords.enabled) lat=\(coords.lat) lon=\(coords.lon)")
return .coordinateWriteFailed("写入后回读不一致")
}
// Step D: verify data rewriting via Go test patch
log("[步骤 D] 验证定位数据改写…")
let result = CoreBridge.testWlocPatch(lat: testLat, lon: testLon, accuracy: 25)
log(" \(result)")
if result.hasPrefix("ok:") {
log(" ✓ 定位数据改写验证通过")
} else {
log(" ✗ 定位数据改写失败")
return .patchFailed(result)
}
log("")
log("======== 环境检测通过 ✓ ========")
return .success
+12 -4
View File
@@ -45,15 +45,23 @@
<table>
<tr>
<th>应用主界面</th>
<th>钉钉</th>
<th>微信</th>
</tr>
<tr>
<td><img src="images/主界面.jpg" alt="iOS 虚拟定位应用主界面" width="180"></td>
<td><img src="images/钉钉.jpg" alt="钉钉虚拟定位打卡" width="180"></td>
<td><img src="images/微信.jpg" alt="微信虚拟定位" width="180"></td>
</tr>
<tr>
<th>Apple 地图</th>
<th>高德地图</th>
<th>Apple Watch</th>
</tr>
<tr>
<td><img src="images/主界面.jpg" alt="iOS 虚拟定位应用主界面" width="210"></td>
<td><img src="images/Apple%20Map.jpg" alt="iPhone Location Spoofer Apple Maps 效果" width="210"></td>
<td><img src="images/高德地图.jpg" alt="Fake GPS 高德地图定位效果" width="210"></td>
<td><img src="images/高血压.jpg" alt="Apple Watch 地区功能验证" width="210"></td>
<td><img src="images/Apple%20Map.jpg" alt="Apple Maps 定位效果" width="180"></td>
<td><img src="images/高德地图.jpg" alt="高德地图定位效果" width="180"></td>
<td><img src="images/高血压.jpg" alt="Apple Watch 地区功能验证" width="180"></td>
</tr>
</table>
@@ -6,5 +6,7 @@
<array>
<string>group.com.paopaolabs.location-spoofer</string>
</array>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
</dict>
</plist>
+2
View File
@@ -14,5 +14,7 @@
</array>
<key>get-task-allow</key>
<true/>
<key>com.apple.developer.networking.wifi-info</key>
<true/>
</dict>
</plist>
+98 -11
View File
@@ -2,26 +2,105 @@ import Foundation
/// GCJ-02 () WGS-84
///
/// MKMapView 使 (AutoNavi) GCJ-02
/// MKMapView `centerCoordinate``convert(point:toCoordinateFrom:)`
/// GCJ-02 CoreLocation / CLLocationManager
/// Apple wloc 使 WGS-84
///
///
/// - UI GCJ-02
/// - wloc WGS-84
///
/// UI GCJ-02 WGS-84
/// MKMapView GCJ-02 Apple WGS-84
/// App WGS-84
enum CoordinateConverter {
// (Krasovsky 1940)
private static let a = 6378245.0
private static let ee = 0.00669342162296594323
///
enum CoordType: String {
case gcj02 = "GCJ-02"
case wgs84 = "WGS-84"
}
// MARK: -
///
@MainActor static var currentTileType = CoordType.gcj02
///
@MainActor
static func updateTileType(lat: Double, lon: Double) {
let type = detectType(lat: lat, lon: lon)
if type != currentTileType {
currentTileType = type
RuntimeLogger.info("APP", "坐标转换", "地图瓦片切换 → \(type.rawValue)", details: [
"坐标": "\(lat), \(lon)"
])
}
}
// MARK: -
/// WGS-84
@MainActor
static func toStored(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
updateTileType(lat: lat, lon: lon)
guard currentTileType == .gcj02 else {
RuntimeLogger.info("APP", "坐标转换", "toStored: WGS-84 → 不转", details: [
"lat": String(lat), "lon": String(lon)
])
return (lat, lon)
}
let wgs = gcj02ToWgs84(lat: lat, lon: lon)
let d = distance(lat1: lat, lon1: lon, lat2: wgs.lat, lon2: wgs.lon)
RuntimeLogger.info("APP", "坐标转换", "toStored: GCJ-02 → WGS-84", details: [
"原始": "\(lat), \(lon)",
"结果": "\(wgs.lat), \(wgs.lon)",
"偏移": String(format: "%.0fm", d)
])
return wgs
}
/// WGS-84
@MainActor
static func toDisplay(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
guard currentTileType == .gcj02 else {
RuntimeLogger.info("APP", "坐标转换", "toDisplay: WGS-84 → 不转", details: [
"lat": String(lat), "lon": String(lon)
])
return (lat, lon)
}
let gcj = wgs84ToGcj02(lat: lat, lon: lon)
let d = distance(lat1: lat, lon1: lon, lat2: gcj.lat, lon2: gcj.lon)
RuntimeLogger.info("APP", "坐标转换", "toDisplay: WGS-84 → GCJ-02", details: [
"原始": "\(lat), \(lon)",
"结果": "\(gcj.lat), \(gcj.lon)",
"偏移": String(format: "%.0fm", d)
])
return gcj
}
// MARK: -
/// Haversine
static func distance(lat1: Double, lon1: Double, lat2: Double, lon2: Double) -> Double {
let r = 6371000.0
let dLat = (lat2 - lat1) * .pi / 180.0
let dLon = (lon2 - lon1) * .pi / 180.0
let a = sin(dLat / 2) * sin(dLat / 2)
+ cos(lat1 * .pi / 180.0) * cos(lat2 * .pi / 180.0)
* sin(dLon / 2) * sin(dLon / 2)
return r * 2 * atan2(sqrt(a), sqrt(1 - a))
}
/// MKMapView GCJ-02 WGS-84
static func detectType(lat: Double, lon: Double) -> CoordType {
// GCJ-02
if lat > 17.5 && lat < 54.0 && lon > 72.5 && lon < 136.0 {
return .gcj02
}
return .wgs84
}
// MARK: -
/// GCJ-02 WGS-84 0.5
static func gcj02ToWgs84(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
var wgsLat = lat
var wgsLon = lon
//
for _ in 0..<2 {
let d = delta(lat: wgsLat, lon: wgsLon)
wgsLat = lat - d.lat
@@ -30,6 +109,14 @@ enum CoordinateConverter {
return (wgsLat, wgsLon)
}
/// WGS-84 GCJ-02
static func wgs84ToGcj02(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
let d = delta(lat: lat, lon: lon)
return (lat + d.lat, lon + d.lon)
}
// MARK: -
/// (WGS-84 GCJ-02 )
private static func delta(lat: Double, lon: Double) -> (lat: Double, lon: Double) {
let dLat = transformLat(x: lon - 105.0, y: lat - 35.0)
+41 -2
View File
@@ -1,5 +1,6 @@
import Network
import Foundation
import SystemConfiguration.CaptiveNetwork
@MainActor
final class NetworkMonitor: ObservableObject {
@@ -7,20 +8,58 @@ final class NetworkMonitor: ObservableObject {
@Published private(set) var isSatisfied = true
@Published private(set) var isWiFiEnabled = true
@Published private(set) var currentSSID: String?
/// WiFi SSID 使
var onWiFiChanged: (() -> Void)?
private let monitor = NWPathMonitor()
private var ssidTimer: Timer?
private var wasSatisfied = true
private init() {
monitor.pathUpdateHandler = { [weak self] path in
let satisfied = path.status == .satisfied
let wifi = path.usesInterfaceType(.wifi)
Task { @MainActor in
self?.isSatisfied = satisfied
self?.isWiFiEnabled = wifi
guard let self else { return }
//
let reconnected = satisfied && !self.wasSatisfied && wifi
self.wasSatisfied = satisfied
self.isSatisfied = satisfied
self.isWiFiEnabled = wifi
if reconnected {
self.onWiFiChanged?()
}
}
}
monitor.start(queue: .main)
startSSIDPolling()
}
var isAirplaneMode: Bool { !isSatisfied }
private func startSSIDPolling() {
ssidTimer = Timer.scheduledTimer(withTimeInterval: 3.0, repeats: true) { [weak self] _ in
Task { @MainActor in
guard let self else { return }
let ssid = Self.fetchSSID()
if ssid != self.currentSSID, ssid != nil {
self.currentSSID = ssid
self.onWiFiChanged?()
}
}
}
}
static func fetchSSID() -> String? {
guard let interfaces = CNCopySupportedInterfaces() as? [String] else { return nil }
for iface in interfaces {
if let info = CNCopyCurrentNetworkInfo(iface as CFString) as? [String: Any],
let ssid = info[kCNNetworkInfoKeySSID as String] as? String {
return ssid
}
}
return nil
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 280 KiB

After

Width:  |  Height:  |  Size: 274 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 292 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 913 KiB

After

Width:  |  Height:  |  Size: 317 KiB