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
}
+2 -54
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)"
}
trustState = .trusted
message = "✓ 定位环境正常(返回 \(status))"
} 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