Files
location-spoofer/App/FirstSetupView.swift
T
xweiba e137ff10e9 release: PaopaoLocationSpoofer v1.0.0
- iOS 虚拟定位工具,基于本地 HTTP 代理 MITM 方案
- MapKit 原生地图体验,支持搜索、收藏、实时定位
- 完整的设置引导流程(证书安装、WiFi 代理配置、环境验证)
- 支持 iOS 15+,SwiftUI 构建
2026-08-05 15:39:57 +08:00

302 lines
15 KiB
Swift

import SwiftUI
enum SetupStep: Int, CaseIterable {
case cert = 0, proxy = 1, verify = 2
var title: String {
switch self {
case .cert: return "初始化 CA 证书"
case .proxy: return "初始化代理"
case .verify: return "环境检测"
}
}
}
struct FirstSetupView: View {
@ObservedObject var setup: SetupCoordinator
let onComplete: () -> Void
@State private var step: SetupStep = .cert
@State private var downloadedDone = false
@State private var installedDone = false
@State private var trustedDone = false
@State private var proxyDone = false
@State private var testPassed: VerificationResult? = nil
@State private var testMessage = ""
@State private var isLoading = false
@State private var manualHint = ""
private var certDone: Bool { downloadedDone && installedDone && trustedDone }
var body: some View {
VStack(spacing: 0) {
// 顶部大步骤进度
HStack(spacing: 6) {
ForEach(SetupStep.allCases, id: \.rawValue) { s in
VStack(spacing: 4) {
Circle()
.fill(s.rawValue < step.rawValue ? Color.green
: s.rawValue == step.rawValue ? Color.blue
: Color.gray.opacity(0.3))
.frame(width: 10, height: 10)
Text(s.title).font(.caption2).foregroundStyle(.secondary)
}
if s.rawValue < SetupStep.allCases.count - 1 {
Rectangle().fill(s.rawValue < step.rawValue ? Color.green : Color.gray.opacity(0.3))
.frame(height: 2).frame(maxWidth: 30)
}
}
}
.padding(.top, 18)
ScrollView {
VStack(alignment: .leading, spacing: 20) {
switch step {
case .cert: certStepView
case .proxy: proxyStepView
case .verify: verifyStepView
}
}
.padding(20)
}
// 底部导航
Divider()
HStack {
if step.rawValue > 0 {
Button("上一步") { step = SetupStep(rawValue: step.rawValue - 1)! }
.buttonStyle(.bordered)
}
Spacer()
if step == .verify && testPassed?.isSuccess == true {
Button("完成,进入主页") { onComplete() }.buttonStyle(.borderedProminent)
} else if step == .cert && certDone {
Button("下一步") { step = .proxy }.buttonStyle(.borderedProminent)
} else if step == .proxy && proxyDone {
Button("下一步") { step = .verify }.buttonStyle(.borderedProminent)
}
}
.padding(.horizontal, 20).padding(.vertical, 10)
}
.alert("无法直接跳转", isPresented: Binding(
get: { !manualHint.isEmpty },
set: { if !$0 { manualHint = "" } }
)) {
Button("知道了", role: .cancel) {}
} message: { Text(manualHint) }
}
// MARK: - 第 1 步:初始化 CA 证书(三个步骤平铺)
private var certStepView: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Label("第 1 步:下载证书", systemImage: "arrow.down.circle")) {
VStack(alignment: .leading, spacing: 12) {
Color.clear.frame(height: 0).padding(.top, 2)
Text("虚拟定位需要通过自签 CA 证书来解密和改写定位请求。点击下方按钮,Safari 会打开下载页面。Safari 弹出「此网站正尝试下载一个配置描述文件」时,点「允许」。")
.font(.caption).foregroundStyle(.secondary)
HStack(spacing: 10) {
Button {
Task { await setup.proxy.openCertificateDownload() }
} label: {
Label("去下载", systemImage: "arrow.down.circle.fill").frame(maxWidth: .infinity)
}.buttonStyle(.borderedProminent).tint(.blue)
Button {
downloadedDone = true
} label: {
Label(downloadedDone ? "已完成 ✓" : "已完成", systemImage: downloadedDone ? "checkmark.circle.fill" : "circle").frame(maxWidth: .infinity)
}.buttonStyle(.bordered).tint(downloadedDone ? .green : .secondary)
}
}
}
GroupBox(label: Label("第 2 步:安装证书", systemImage: "square.and.arrow.down")) {
VStack(alignment: .leading, spacing: 12) {
Color.clear.frame(height: 0).padding(.top, 2)
Text("下载完成后打开系统「设置」:\n\n1. 如果顶部显示了「已下载描述文件」,点进去安装\n2. 如果没显示:进入「通用 → VPN与设备管理」,找到 WLOC CA 证书点击安装\n\n安装时系统会要求输入锁屏密码,确认后点右上角「安装」即可。")
.font(.caption).foregroundStyle(.secondary)
HStack(spacing: 10) {
Button { openSettings(.general) } label: {
Label("去安装", systemImage: "gearshape").frame(maxWidth: .infinity)
}.buttonStyle(.borderedProminent).tint(.blue)
Button { installedDone = true } label: {
Label(installedDone ? "已完成 ✓" : "已完成", systemImage: installedDone ? "checkmark.circle.fill" : "circle").frame(maxWidth: .infinity)
}.buttonStyle(.bordered).tint(installedDone ? .green : .secondary)
}
}
}
GroupBox(label: Label("第 3 步:信任证书", systemImage: "shield.checkered")) {
VStack(alignment: .leading, spacing: 12) {
Color.clear.frame(height: 0).padding(.top, 2)
Text("证书安装后还需要开启信任,否则系统会拦截代理的 HTTPS 请求:\n\n1. 打开「设置 → 通用 → 关于本机」\n2. 滑到底部找到「证书信任设置」\n3. 找到 WLOC CA,打开旁边的开关\n4. 弹出的警告中点「继续」\n\n⚠️ 每次重装 App 都需要重新下载安装证书。如果检测时报 TLS 错误,说明证书过期或不匹配,请删除旧证书(设置 → 通用 → VPN与设备管理)后重新安装。")
.font(.caption).foregroundStyle(.secondary)
HStack(spacing: 10) {
Button { openSettings(.general) } label: {
Label("去信任", systemImage: "shield.checkered").frame(maxWidth: .infinity)
}.buttonStyle(.borderedProminent).tint(.blue)
Button { trustedDone = true } label: {
Label(trustedDone ? "已完成 ✓" : "已完成", systemImage: trustedDone ? "checkmark.circle.fill" : "circle").frame(maxWidth: .infinity)
}.buttonStyle(.bordered).tint(trustedDone ? .green : .secondary)
}
}
}
}
}
// MARK: - 第 2 步:初始化代理
private var proxyStepView: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Label("配置 WiFi 系统代理", systemImage: "wifi")) {
VStack(alignment: .leading, spacing: 12) {
Color.clear.frame(height: 0).padding(.top, 2)
Text("要让系统定位请求经过本地代理,需要在 WiFi 设置中手动配置:\n\n1. 打开「设置 → 无线局域网」\n2. 点击当前连接的 WiFi 右侧 (i) 图标\n3. 滑到底部找到「HTTP 代理」,选择「手动」\n4. 服务器填入 127.0.0.1,端口填入 8888\n5. 点右上角「存储」\n\n⚠️ 只有连接的这个 WiFi 会走代理,蜂窝数据不受影响。换 WiFi 后需要重新配置。")
.font(.caption).foregroundStyle(.secondary)
}
}
HStack(spacing: 12) {
Button {
UIPasteboard.general.string = "127.0.0.1:8888"
} label: {
Label("复制地址", systemImage: "doc.on.doc")
.frame(maxWidth: .infinity)
}.buttonStyle(.bordered).tint(.blue)
Button {
openSettings(.wifi)
} label: {
Label("去设置", systemImage: "wifi")
.frame(maxWidth: .infinity)
}.buttonStyle(.borderedProminent).tint(.blue)
}
Button {
proxyDone = true
} label: {
Label(proxyDone ? "已完成 ✓" : "已完成", systemImage: proxyDone ? "checkmark.circle.fill" : "circle")
.frame(maxWidth: .infinity)
}.buttonStyle(.bordered).tint(proxyDone ? .green : .secondary)
}
}
// MARK: - 第 3 步:环境检测
private var verifyStepView: some View {
VStack(alignment: .leading, spacing: 20) {
GroupBox(label: Label("检测整个流程", systemImage: "checklist")) {
VStack(alignment: .leading, spacing: 12) {
Color.clear.frame(height: 0).padding(.top, 2)
Text("点击「开始检测」后依次检查代理运行、证书信任与 WiFi 代理配置、坐标写入、数据改写。全部通过后「完成,进入主页」按钮才会亮起。")
.font(.caption).foregroundStyle(.secondary)
}
}
if let result = testPassed, result.isSuccess {
HStack(spacing: 10) {
Image(systemName: "checkmark.circle.fill").foregroundStyle(.green).font(.title3)
Text("全部检测通过").font(.subheadline.weight(.semibold)).foregroundStyle(.green)
}.padding(12).background(Color.green.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
} else if let result = testPassed {
VStack(alignment: .leading, spacing: 10) {
HStack(spacing: 10) {
Image(systemName: "xmark.circle.fill").foregroundStyle(.red).font(.title3)
VStack(alignment: .leading, spacing: 2) {
Text("测试未通过").font(.subheadline.weight(.semibold)).foregroundStyle(.red)
Text(failureSummary(result)).font(.caption).foregroundStyle(.secondary)
}
}
if let step = failureRemedyStep(result) {
Button {
withAnimation(.none) { self.step = step }
testPassed = nil
testMessage = ""
} label: {
Label("去处理", systemImage: "arrow.right.circle.fill")
.font(.subheadline.weight(.medium))
.frame(maxWidth: .infinity)
.padding(.vertical, 10)
}
.buttonStyle(.borderedProminent)
.tint(.red)
}
}
.padding(12)
.background(Color.red.opacity(0.08), in: RoundedRectangle(cornerRadius: 10))
}
if !testMessage.isEmpty {
ScrollView {
Text(testMessage)
.font(.caption.monospaced()).textSelection(.enabled)
.padding(10).frame(maxWidth: .infinity, alignment: .leading)
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 8))
}.frame(maxHeight: 280)
}
Button {
isLoading = true; testMessage = ""
Task {
testPassed = await setup.runVerificationTest()
testMessage = setup.testLog
isLoading = false
}
} label: {
HStack {
if isLoading { ProgressView().tint(.white).controlSize(.small) }
Text(buttonText).frame(maxWidth: .infinity)
}
}
.buttonStyle(.borderedProminent)
.tint(buttonTint)
.disabled(isLoading)
Button {
UIPasteboard.general.string = testMessage
} label: {
Label("复制检测日志", systemImage: "doc.on.doc").frame(maxWidth: .infinity)
}.buttonStyle(.bordered).disabled(testMessage.isEmpty)
}
}
private var buttonText: String {
guard let result = testPassed else { return "开始检测" }
return result.isSuccess ? "重新检测" : "⚠️ 重新测试"
}
private var buttonTint: Color {
guard let result = testPassed else { return .blue }
return result.isSuccess ? .blue : .red
}
private func failureSummary(_ result: VerificationResult) -> String {
switch result {
case .proxyNotRunning: return "代理未能启动,请检查代理状态"
case .verificationInProgress: return "已有检测正在进行,请稍候"
case .verificationSuperseded: return "检测期间位置已更新,本次结果已取消,请重新检测"
case .certNotTrusted: return "CA 证书未安装或未信任,请重新安装证书并开启信任"
case .wifiProxyNotConfigured: return "WiFi 代理未配置,请在系统设置中配置 127.0.0.1:8888"
case .coordinateWriteFailed: return "坐标写入失败,请重试"
case .patchFailed: return "坐标改写验证失败,可能是证书过期或不匹配"
case .success: return ""
}
}
private func failureRemedyStep(_ result: VerificationResult) -> SetupStep? {
switch result {
case .certNotTrusted: return .cert
case .wifiProxyNotConfigured: return .proxy
case .proxyNotRunning, .verificationInProgress, .verificationSuperseded,
.coordinateWriteFailed, .patchFailed: return nil
case .success: return nil
}
}
@MainActor
private func openSettings(_ destination: SystemSettingsDestination) {
SystemSettingsNavigator.open(destination) { fallbackHint in
if let fallbackHint { manualHint = fallbackHint }
}
}
}