import SwiftUI struct SettingsView: View { @ObservedObject var setup: SetupCoordinator @ObservedObject var actions: LocationActionCoordinator @ObservedObject private var proxy = ProxyManager.shared @ObservedObject private var runtimeMode = ProxyRuntimeModeStore.shared @ObservedObject private var thirdPartyProxy = ThirdPartyProxyManager.shared @ObservedObject private var thirdPartyClient = ThirdPartyProxyClientStore.shared @ObservedObject private var motionSimulation = MotionSimulationStore.shared @ObservedObject private var moduleSource = ThirdPartyModuleSourceStore.shared @Environment(\.dismiss) private var dismiss @State private var activeTip: TipKind? @State private var proxyOperationError = "" @State private var proxyOperationAlertTitle = "代理操作失败" @State private var modeOperationRunning = false @State private var copiedClient: ThirdPartyProxyClient? @State private var showCertificateResetConfirmation = false @State private var githubDestination: SafariDestination? var body: some View { Form { Section("运行模式") { Picker("模式", selection: runtimeModeBinding) { ForEach(ProxyRuntimeMode.allCases) { mode in Text(mode.displayName).tag(mode) } } .pickerStyle(.inline) .disabled(modeOperationRunning || actions.state.isBusy || thirdPartyProxy.isRequesting) } Section("状态") { if runtimeMode.mode == .localWiFi { HStack { Label("本机代理", systemImage: proxy.isRunning ? "play.circle.fill" : "stop.circle") Spacer() Toggle("", isOn: proxyBinding).labelsHidden() .tint(.blue) .disabled(actions.state.isBusy) } } else { HStack { Label("第三方模块", systemImage: thirdPartyStatusIcon) Spacer() Text(thirdPartyStatusText).foregroundStyle(.secondary) } Button { detectThirdPartyConnection() } label: { if thirdPartyProxy.isRequesting { HStack { ProgressView(); Text("正在检测…") } } else { Label("检测连接", systemImage: "network") } } .disabled(thirdPartyProxy.isRequesting) } HStack { Label("虚拟定位", systemImage: virtualLocationIsActive ? "location.fill" : "location.slash") Spacer() Text(virtualLocationStatusText).foregroundStyle(.secondary) } } Section("定位模拟") { Toggle("运动状态模拟", isOn: motionSimulationBinding) .disabled(modeOperationRunning || actions.state.isBusy || thirdPartyProxy.isRequesting) Text("实验性功能,默认关闭。开启后会同时模拟定位响应中的运动状态。") .font(.footnote) .foregroundStyle(.secondary) } if runtimeMode.mode == .thirdParty { thirdPartyConfigurationSection } else { Section("说明") { Button { activeTip = .activation } label: { Label("生效说明", systemImage: "checklist") } Button { activeTip = .deactivation } label: { Label("失效说明", systemImage: "arrow.uturn.backward.circle") } Button { activeTip = .removeProxy } label: { Label("关闭 WiFi 代理", systemImage: "wifi.slash") } } } Section("工作原理") { Text(workflowDescription) .font(.footnote) .foregroundStyle(.secondary) } Section("应用") { if runtimeMode.mode == .localWiFi { Button { setup.requestSetup() } label: { Label("进入引导页", systemImage: "arrow.clockwise.circle") } } valueRow("版本", value: versionText) } if runtimeMode.mode == .localWiFi { Section("证书") { Button(role: .destructive) { showCertificateResetConfirmation = true } label: { Label("重置证书", systemImage: "arrow.clockwise.circle") } .disabled(modeOperationRunning || actions.state.isBusy) Text("仅删除 App 钥匙串中的设备 CA。iOS 中已经安装的旧证书需要在系统设置里手动移除。") .font(.footnote) .foregroundStyle(.secondary) } } Section("支持") { NavigationLink { BugReportView(setup: setup) } label: { Label("报告 Bug", systemImage: "ladybug") } Button { githubDestination = SafariDestination(url: GitHubSubmission.usageHelpURL) } label: { Label("使用帮助", systemImage: "questionmark.circle") } Button { githubDestination = SafariDestination(url: GitHubSubmission.featureRequestURL) } label: { Label("功能建议", systemImage: "lightbulb") } if runtimeMode.mode == .thirdParty { Button { UIPasteboard.general.string = GitHubSubmission.communityContributionTemplate( for: thirdPartyClient.selectedClient, systemVersion: UIDevice.current.systemVersion ) githubDestination = SafariDestination( url: GitHubSubmission.communityContributionURL ) } label: { Label("分享第三方配置", systemImage: "square.and.arrow.up") } } } Section("关于") { Button { if let url = URL(string: "https://github.com/xweiba/location-spoofer") { UIApplication.shared.open(url) } } label: { Label("xweiba/location-spoofer", systemImage: "link") } Text("如果觉得好用,欢迎去 GitHub 给项目点个 Star") .font(.caption2) .foregroundStyle(.secondary) } Section("致谢") { Button { if let url = URL(string: "https://github.com/Yu9191/wloc") { UIApplication.shared.open(url) } } label: { Label("核心定位改写逻辑移植自 Yu9191/wloc", systemImage: "heart.fill") .foregroundStyle(.pink) } } } .navigationTitle("设置") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("完成") { dismiss() } } } .sheet(item: $activeTip) { kind in TipSheetView(kind: kind) } .sheet(item: $githubDestination) { destination in SafariView(url: destination.url) .ignoresSafeArea() } .alert(proxyOperationAlertTitle, isPresented: Binding( get: { !proxyOperationError.isEmpty }, set: { if !$0 { proxyOperationError = "" } } )) { Button("知道了", role: .cancel) {} } message: { Text(proxyOperationError) } .confirmationDialog( "重置证书?", isPresented: $showCertificateResetConfirmation, titleVisibility: .visible ) { Button("重置并生成新证书", role: .destructive) { resetCertificateAuthority() } Button("取消", role: .cancel) {} } message: { Text("当前虚拟定位和本地代理将停止。App 会删除钥匙串中的设备 CA、立即生成新证书,并打开安装与信任引导。你还需要前往 iOS「设置 → 通用 → VPN 与设备管理」手动删除旧证书,然后重新下载安装并完全信任新证书。") } } private func valueRow(_ title: String, value: String) -> some View { HStack { Text(title); Spacer(); Text(value).font(.footnote.monospaced()).foregroundStyle(.secondary) } } private var versionText: String { let v = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "?" let b = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "?" return "\(v) (\(b))" } private var proxyBinding: Binding { Binding(get: { proxy.isRunning }, set: { on in Task { if on { do { try await proxy.start() } catch { proxy.error = error.localizedDescription proxyOperationAlertTitle = "代理操作失败" proxyOperationError = error.localizedDescription } } else { if actions.virtualLocationEnabled { actions.clear() RuntimeLogger.info("APP", "Settings", "关闭代理前已同步关闭虚拟定位") } proxy.stop() } } }) } private var runtimeModeBinding: Binding { Binding( get: { runtimeMode.mode }, set: { newMode in switchRuntimeMode(to: newMode) } ) } private var motionSimulationBinding: Binding { Binding( get: { motionSimulation.isEnabled }, set: { enabled in if runtimeMode.mode == .localWiFi { proxy.applyMotionSimulation(enabled) return } guard thirdPartyProxy.activeSettings?.success == true else { motionSimulation.setEnabled(enabled) return } modeOperationRunning = true Task { @MainActor in do { _ = try await thirdPartyProxy.updateMotionSimulation(enabled) motionSimulation.setEnabled(enabled) } catch { RuntimeLogger.error( "APP", "ThirdPartyProxy", "同步运动状态设置失败", error: error, details: ["当前客户端": thirdPartyClient.selectedClient.name] ) setup.requestThirdPartySetup(message: error.localizedDescription) } modeOperationRunning = false } } ) } @ViewBuilder private var thirdPartyConfigurationSection: some View { Section("第三方代理配置") { Picker("客户端", selection: Binding( get: { thirdPartyClient.selectedClient }, set: { thirdPartyClient.select($0) } )) { ForEach(ThirdPartyProxyClient.allCases) { client in Text(client.name).tag(client) } } Toggle("使用国内镜像下载模块", isOn: Binding( get: { moduleSource.useMirror }, set: { moduleSource.setUseMirror($0) } )) Text("仅影响之后复制和重新导入的模块地址;已安装模块需要重新导入后切换来源。") .font(.footnote) .foregroundStyle(.secondary) if let verificationText = thirdPartyClient.selectedClient.verificationText { HStack { Text("验证状态") Spacer() Text(verificationText) .font(.footnote) .foregroundStyle(.orange) } } Button { UIPasteboard.general.string = thirdPartyClient.selectedClient.subscriptionURL.absoluteString copiedClient = thirdPartyClient.selectedClient } label: { Label(copiedClient == thirdPartyClient.selectedClient ? "已复制模块订阅地址" : "复制模块订阅地址", systemImage: "doc.on.doc") } Button { openThirdPartyClient(thirdPartyClient.selectedClient) } label: { Label("打开 \(thirdPartyClient.selectedClient.name)", systemImage: "arrow.up.forward.app") } Button { setup.requestThirdPartyOnboarding() dismiss() } label: { Label("重新打开配置引导", systemImage: "arrow.clockwise.circle") } if thirdPartyClient.selectedClient == .egern { Text("Egern 直接使用 Surge 的 .sgmodule 模块。") .font(.footnote).foregroundStyle(.secondary) } else if thirdPartyClient.selectedClient == .stash { Text("Stash 直接订阅 .stoverride,不要通过 Script Hub 转换。") .font(.footnote).foregroundStyle(.secondary) } Text("复制模块订阅地址后,在对应代理客户端中添加模块/重写订阅,并启用 MITM。第三方客户端保存坐标后,即使关闭本 App,坐标仍由代理客户端持久化并继续生效。") .font(.footnote).foregroundStyle(.secondary) } } private var thirdPartyStatusIcon: String { switch thirdPartyProxy.connectionState { case .unknown: return "questionmark.circle" case .connected: return "checkmark.circle.fill" case .failed: return "xmark.circle.fill" } } private var thirdPartyStatusText: String { switch thirdPartyProxy.connectionState { case .unknown: return "未检测" case .connected(let active): return active ? "已连接,有坐标" : "已连接,无坐标" case .failed: return "连接失败" } } private var virtualLocationStatusText: String { if runtimeMode.mode == .localWiFi { return actions.virtualLocationEnabled ? "已开启" : "已关闭" } if case .connected(let active) = thirdPartyProxy.connectionState { return active ? "第三方已保存" : "未保存" } return "未知" } private var workflowDescription: String { if runtimeMode.mode == .thirdParty { return "App 只负责地图选点、收藏和发送 WGS-84 坐标。第三方代理客户端通过模块拦截 Apple WLOC 请求并持久化当前坐标;本模式不启动本机代理,不使用 App 的 CA,也不需要配置 127.0.0.1:8888。" } return """ App 在设备本地运行一个代理服务器(127.0.0.1:8888)。 通过 WiFi 手动代理配置,让系统的定位请求(gs-loc.apple.com/clls/wloc)经过这个本地代理。代理使用已安装的 CA 证书对 HTTPS 流量做中间人解密,把 Apple 返回的定位坐标改写为你设置的虚拟坐标,再加密返回给系统,从而实现虚拟定位。 """ } private func switchRuntimeMode(to newMode: ProxyRuntimeMode) { guard newMode != runtimeMode.mode, !modeOperationRunning else { return } modeOperationRunning = true Task { @MainActor in defer { modeOperationRunning = false } switch newMode { case .thirdParty: if actions.virtualLocationEnabled { actions.clear() } proxy.stop() setup.completeSetup() runtimeMode.setMode(.thirdParty) if runtimeMode.isInitialized(.thirdParty) { do { _ = try await thirdPartyProxy.validateVersion() _ = try await thirdPartyProxy.query() proxyOperationAlertTitle = "模式已切换" proxyOperationError = "第三方代理模式检测通过。请关闭 Wi-Fi 中的 127.0.0.1:8888 手动代理,避免双重拦截。" } catch { openThirdPartySetup(for: error) } } else { setup.requestThirdPartyOnboarding() dismiss() } case .localWiFi: do { try await thirdPartyProxy.clear() } catch { RuntimeLogger.warning("APP", "Mode", "切换 APP 模式前无法清除第三方坐标", details: [ "错误": error.localizedDescription ]) } runtimeMode.setMode(.localWiFi) await setup.prepareLocalServices() if runtimeMode.isInitialized(.localWiFi) { let result = await setup.runVerificationTest() setup.applyVerificationResult(result) if result.isSuccess { proxyOperationAlertTitle = "模式已切换" proxyOperationError = "APP 模式环境检测通过。请停用第三方 WLOC 模块或代理连接,避免双重拦截。" } else { dismiss() } } else { setup.requestSetup() dismiss() } } } } private func detectThirdPartyConnection() { let client = thirdPartyClient.selectedClient let startedAt = Date() Task { @MainActor in do { _ = try await thirdPartyProxy.validateVersion() _ = try await thirdPartyProxy.query() RuntimeLogger.info("APP", "ThirdPartyProxy", "设置页第三方连接检测通过", details: [ "当前客户端": client.name, "请求动作": "WLOC query", "耗时毫秒": String(Int(Date().timeIntervalSince(startedAt) * 1_000)) ]) runtimeMode.markInitialized(.thirdParty) } catch { RuntimeLogger.error( "APP", "ThirdPartyProxy", "设置页第三方连接检测失败", error: error, details: [ "当前客户端": client.name, "请求动作": "WLOC query", "连接状态": String(describing: thirdPartyProxy.connectionState), "耗时毫秒": String(Int(Date().timeIntervalSince(startedAt) * 1_000)), "处理建议": "检查模块、MITM、证书和代理/VPN连接" ] ) openThirdPartySetup(for: error) } } } private func openThirdPartySetup(for error: Error) { setup.requestThirdPartySetup(message: error.localizedDescription) dismiss() } private func resetCertificateAuthority() { guard runtimeMode.mode == .localWiFi, !modeOperationRunning else { return } modeOperationRunning = true Task { @MainActor in defer { modeOperationRunning = false } if actions.virtualLocationEnabled { actions.clear() } proxy.stop() do { try setup.certificateStore.reset() runtimeMode.resetInitialization(.localWiFi) guard await setup.prepareLocalServices() else { proxyOperationAlertTitle = "证书重置失败" proxyOperationError = setup.message return } setup.requestCertificateSetup() dismiss() } catch { proxyOperationAlertTitle = "证书重置失败" proxyOperationError = error.localizedDescription } } } private func openThirdPartyClient(_ client: ThirdPartyProxyClient) { guard let url = client.launchURL else { return } UIApplication.shared.open(url, options: [:]) { opened in guard !opened else { return } Task { @MainActor in proxyOperationAlertTitle = "无法打开客户端" proxyOperationError = "无法打开 \(client.name),请确认客户端已安装后手动打开。" } } } private var virtualLocationIsActive: Bool { if runtimeMode.mode == .localWiFi { return actions.virtualLocationEnabled } if case .connected(let active) = thirdPartyProxy.connectionState { return active } return false } }