← API | 列表 | swift_ios项目架构设计
提示信息
# 同伴 App — iOS Swift 项目架构设计

> 从零搭建原生 Swift 项目的完整指引。基于 Xcode 26 / Swift 6.3 / iOS 26,采用 SwiftUI-First + Liquid Glass 设计语言。

---

## 一、技术选型

| 层次 | 方案 | 说明 |
|---|---|---|
| UI 框架 | **SwiftUI**(主)+ UIKit(必要时) | iOS 26 SwiftUI 已完全成熟,Liquid Glass 原生支持 |
| 语言 | **Swift 6.3** | 严格并发检查默认开启,`@MainActor` 全面使用 |
| 状态管理 | **@Observable + Swift Concurrency** | iOS 26 中 @Observable 同时驱动 SwiftUI 和 UIKit 更新 |
| 依赖管理 | **CocoaPods**(主)| OpenIM、微信、支付宝 SDK 必须用 Pod |
| 路由 | **NavigationStack + Coordinator** | SwiftUI NavigationStack + 全局 Coordinator |
| 架构 | **MVVM + Repository** | ViewModel 对应 Flutter Controller |
| 异步 | **Swift Concurrency**(async/await + Actor) | Swift 6 严格模式,Actor 隔离 |
| 最低系统 | **iOS 26.0** | Liquid Glass API 要求 iOS 26+ |

---

## 二、Xcode 26 / Swift 6.3 对项目的影响

### 2.1 最低系统版本调整

从原规划的 iOS 13 **升至 iOS 26**。理由:
- Liquid Glass API(`glassEffect()`、`UIGlassEffect`、新 TabBar)全部要求 iOS 26+
- 无需做任何降级兼容代码
- iOS 26 发布后旧机型用户可通过系统升级覆盖(A12 以上芯片支持 iOS 26)

### 2.2 Swift 6.3 严格并发

Swift 6.x 默认开启严格并发检查,影响所有网络/状态代码:

```swift
// ❌ Swift 6 会报错(跨 actor 边界读取非 Sendable 属性)
class AuthViewModel {
    var user: LoginUser?
    func login() async { ... }  // 不明确是哪个 actor
}

// ✅ 正确写法:明确 @MainActor 隔离 UI 状态
@MainActor
@Observable
final class AuthViewModel {
    var user: LoginUser?
    var isLoading = false

    func login(phone: String, code: String) async {
        isLoading = true
        defer { isLoading = false }
        // async 操作自动在 MainActor 上下文中
        user = try? await AuthRepository.shared.login(phone: phone, code: code)
    }
}

// Repository 层不需要 @MainActor,用 actor 保证线程安全
actor AuthRepository {
    static let shared = AuthRepository()
    func login(phone: String, code: String) async throws -> LoginUser { ... }
}
```

Swift 6.3 本身(相比 6.0/6.2)的新特性主要是 C 互操作(`@c` 属性)和嵌入式 Swift 改进,**对普通 iOS App 开发影响不大**。核心是用好 Swift 6.0 引入的严格并发。

### 2.3 @Observable 在 iOS 26 的新能力

iOS 26 中 `@Observable` 同时驱动 **SwiftUI + UIKit** 更新,无需手动 `setNeedsLayout()`:

```swift
@MainActor
@Observable
final class ConversationListViewModel {
    var conversations: [Conversation] = []
    var isLoading = false
}

// UIKit 中自动响应(iOS 26 新特性)
class ConversationListVC: UIViewController {
    let viewModel = ConversationListViewModel()

    // iOS 26 新增:updateProperties() 在 viewWillLayoutSubviews 前自动调用
    override func updateProperties() {
        tableView.isHidden = viewModel.isLoading
    }
}
```

---

## 三、Liquid Glass 设计系统

### 3.1 核心 API 速查

#### SwiftUI

```swift
// 基础:给任意 View 加液态玻璃
Text("信号")
    .padding()
    .glassEffect()                        // 默认 .regular 风格,胶囊形

// 带形状
someView
    .glassEffect(.regular, in: .rect(cornerRadius: 16))

// 高透明变体(适合叠加层)
someView.glassEffect(.clear)

// 交互态(点击缩放 + 光晕)
Button("发射信号") { }
    .glassEffect(.regular.interactive())

// 颜色着色(品牌色)
someView.glassEffect(.regular.tint(.accentColor))
```

#### GlassEffectContainer(多个玻璃元素融合变形)

```swift
// 多个相邻玻璃元素放进容器 → 自动融合边界、共享渲染
GlassEffectContainer {
    HStack {
        Button("收一条") { }
            .glassEffect()
        Button("发一条") { }
            .glassEffect()
    }
}
```

#### UIKit

```swift
// UIGlassEffect — UIVisualEffect 子类
let glass = UIGlassEffect()                   // .regular 风格
let glass = UIGlassEffect(style: .clear)      // 高透明
let effectView = UIVisualEffectView(effect: glass)
effectView.frame = someButton.bounds

// 多玻璃融合(UIKit)
let container = UIGlassContainerEffect()
let containerView = UIVisualEffectView(effect: container)
// 子视图 effectView 嵌套进 containerView.contentView
```

### 3.2 系统组件自动适配

以下组件在 iOS 26 SDK 编译后**自动变成 Liquid Glass**,无需手动处理:

| 组件 | 效果 |
|---|---|
| `TabView` | 浮动胶囊形玻璃 Tab 栏,离屏幕边缘有间距 |
| `NavigationStack` 导航栏 | 玻璃质感,内容滚动时透明度动态变化 |
| `sheet` / `confirmationDialog` | 玻璃底部弹层 |
| `Alert` | 玻璃弹窗 |
| `Menu` | 玻璃菜单 |

### 3.3 不该用 Liquid Glass 的场景

- 全屏沉浸内容(视频播放、全屏图片)不加玻璃
- 已有纯色背景卡片(用 `.background(.background.secondary)` 代替玻璃)
- 文字密集的信息列表行(影响可读性)

---

## 四、CocoaPods 依赖清单(Podfile)

```ruby
platform :ios, '26.0'
use_frameworks!

target 'TongbanApp' do
  # ── 核心 IM ──────────────────────────────────────────────
  pod 'OpenIMSDK', '~> 3.8'

  # ── 存储 ────────────────────────────────────────────────
  pod 'MMKV', '~> 2.0'

  # ── 图片 ────────────────────────────────────────────────
  pod 'Kingfisher', '~> 8.0'
  pod 'TOCropViewController'

  # ── 动画 ────────────────────────────────────────────────
  pod 'lottie-ios', '~> 4.4'

  # ── 业务 SDK ─────────────────────────────────────────────
  pod 'WechatOpenSDK-XCFramework'
  pod 'AlipaySDK-iOS'

  # ── 安全 ────────────────────────────────────────────────
  pod 'IOSSecuritySuite', '~> 1.9'

  # ── 数据库 ────────────────────────────────────────────────
  pod 'GRDB.swift', '~> 6.0'
end
```

> `shimmer/skeletonizer` 不需要引入——SwiftUI 内置 `.redacted(reason: .placeholder)` 直接实现骨架屏。

---

## 五、项目目录结构

```
TongbanApp/
├── App/
│   ├── TongbanApp.swift             # @main 入口(SwiftUI App)
│   ├── AppDelegate.swift            # UIApplicationDelegate(SDK 初始化)
│   └── AppCoordinator.swift         # 全局路由状态
│
├── Core/
│   ├── Network/
│   │   ├── APIClient.swift
│   │   ├── TokenInterceptor.swift
│   │   ├── AuthInterceptor.swift
│   │   ├── APIInterceptor.swift
│   │   └── UploadClient.swift
│   ├── Storage/
│   │   ├── AppStorage.swift
│   │   ├── StorageKeys.swift
│   │   └── UserProfileCache.swift   # GRDB
│   ├── Cache/
│   │   └── MediaCacheService.swift
│   ├── Adapters/
│   │   ├── IMAdapter.swift
│   │   ├── PaymentAdapter.swift
│   │   └── SecurityAdapter.swift
│   ├── Privacy/
│   │   └── PrivacyService.swift
│   └── Models/
│       └── AppAPIError.swift
│
├── Features/
│   ├── Splash/
│   │   └── SplashView.swift
│   ├── Auth/
│   │   ├── LoginView.swift
│   │   ├── CompleteProfileView.swift
│   │   └── AuthViewModel.swift      # @Observable @MainActor
│   ├── Main/
│   │   └── MainTabView.swift        # iOS 26 新 TabView
│   ├── Signal/
│   │   ├── Inbox/
│   │   ├── Publish/
│   │   └── Detail/
│   └── Chat/
│       ├── ConversationList/
│       └── Conversation/
│
├── Theme/
│   ├── AppColors.swift
│   ├── AppFonts.swift
│   └── AppSpacing.swift
│
└── Shared/
    ├── Components/
    └── Extensions/
```

---

## 六、App 入口(SwiftUI App)

```swift
// TongbanApp.swift
@main
struct TongbanApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    @State private var coordinator = AppCoordinator()

    var body: some Scene {
        WindowGroup {
            RootView()
                .environment(coordinator)
        }
    }
}

// RootView.swift — 根据登录态决定展示哪个流程
struct RootView: View {
    @Environment(AppCoordinator.self) private var coordinator

    var body: some View {
        switch coordinator.route {
        case .splash:          SplashView()
        case .privacy:         PrivacyView()
        case .login:           LoginView()
        case .completeProfile: CompleteProfileView()
        case .main:            MainTabView()
        }
    }
}

// AppCoordinator.swift — 全局路由状态
@MainActor
@Observable
final class AppCoordinator {
    enum Route { case splash, privacy, login, completeProfile, main }
    var route: Route = .splash

    func goLogin()           { withAnimation { route = .login } }
    func goMain()            { withAnimation { route = .main } }
    func goCompleteProfile() { withAnimation { route = .completeProfile } }
    func goPrivacy()         { withAnimation { route = .privacy } }
}
```

---

## 七、主 Tab(iOS 26 新 TabView)

系统自动应用 Liquid Glass 浮动 Tab 栏:

```swift
// MainTabView.swift
struct MainTabView: View {
    @State private var selectedTab: Tab = .signal

    enum Tab { case signal, messages, profile }

    var body: some View {
        TabView(selection: $selectedTab) {
            Tab("信号", systemImage: "dot.radiowaves.left.and.right", value: .signal) {
                SignalInboxView()
            }

            Tab("消息", systemImage: "message", value: .messages) {
                ConversationListView()
            }
            .badge(unreadCount)            // 未读数角标

            Tab("我的", systemImage: "person", value: .profile) {
                ProfileView()
            }
        }
        // iOS 26:向下滚动时自动隐藏 Tab 栏
        .tabBarMinimizeBehavior(.onScrollDown)
    }
}
```

---

## 八、本地存储(同前,保持不变)

```swift
// StorageKeys.swift — 与 Flutter 端完全对齐
enum StorageKeys {
    static let accessToken      = "auth:access_token"
    static let refreshToken     = "auth:refresh_token"
    static let userId           = "auth:user_id"
    static let imToken          = "auth:im_token"
    static let userProfile      = "auth:user_profile"
    static let userPhone        = "auth:user_phone"
    static let privacyAgreed    = "privacy:agreed"
    static let deviceId         = "device:id"           // 实际存 Keychain
    static let attRequested     = "att:requested"
    static let appStartConfig   = "app:start_config"
    static let signalFilter     = "signal:filter"
    static let signalInboxCache = "signal:inbox_cache"
    static let convListCache    = "conv_list_cache_v1"

    static func flashViewed(_ msgId: String) -> String { "cache:flash_viewed:\(msgId)" }
    static func imOpened(_ convId: String)   -> String { "im_opened_\(convId)" }
}
```

---

## 九、网络层

### 9.1 必须 Header(AuthInterceptor)

```
X-App-Key       AppConfig.appKey
X-Timestamp     当前 Unix 秒时间戳(String)
X-Nonce         UUID().uuidString(每次不同)
X-Sign          MD5(appKey + timestamp + nonce + sortedParams + appSecret).lowercased hex
X-Device-Id     Keychain 中的设备 UUID
X-OS            "ios"
X-App-Version   Bundle.main 的 CFBundleShortVersionString
X-Language      Locale.current.languageCode ?? "zh"
Authorization   "Bearer \(accessToken)"(已登录时)
```

### 9.2 全局错误码处理

```swift
// code != 0 时的处理规则
switch code {
case 1005: break          // TokenInterceptor 自动静默刷新重试
case 1006:                // 强制退出
    await AuthService.shared.forceLogout()
case 1008:                // 封禁
    coordinator.goBanned(banInfo: data["ban_info"])
case 3008:                // 信号今日上限(全局弹窗)
    showSystemAlert("今日信号已达上限")
default:
    throw AppAPIError(code: code, message: msg)
}
```

### 9.3 Token 自动刷新

```
请求 → code=1002(AccessToken 过期)
  ├─ 未在刷新 → POST /auth/token/refresh(独立 URLSession,无拦截器)
  │     ├─ 成功 → 写 MMKV → 重试原请求 + 所有排队请求
  │     └─ 失败 → forceLogout()
  └─ 刷新中 → 入队等待,刷新完成统一重试
```

---

## 十、启动流程(SplashView)

```swift
// SplashView.swift
struct SplashView: View {
    @Environment(AppCoordinator.self) private var coordinator
    @State private var splashVM = SplashViewModel()

    var body: some View {
        ZStack {
            Color(.systemBackground).ignoresSafeArea()

            VStack(spacing: 48) {
                // 呼吸动画图标
                Image(systemName: "compass.drawing")
                    .font(.system(size: 48))
                    .foregroundStyle(.accent)
                    .symbolEffect(.breathe)

                VStack(spacing: 8) {
                    Text("SIGNAL").font(.custom("Serif", size: 32)).tracking(4)
                    Text("同伴 · 极简社交").font(.caption).foregroundStyle(.secondary)
                }

                ProgressView().tint(.primary)
            }
        }
        .task { await splashVM.start(coordinator: coordinator) }
    }
}

// SplashViewModel.swift
@MainActor
@Observable
final class SplashViewModel {

    func start(coordinator: AppCoordinator) async {
        // 最大超时 10 秒
        await withTaskGroup(of: Void.self) { group in
            group.addTask { await self.runStateMachine(coordinator: coordinator) }
            group.addTask {
                try? await Task.sleep(for: .seconds(10))
                await self.navigateByToken(coordinator: coordinator)
            }
            await group.next()
            group.cancelAll()
        }
    }

    private func runStateMachine(coordinator: AppCoordinator) async {
        // 1. 隐私协议
        guard PrivacyService.hasAgreed else {
            coordinator.goPrivacy(); return
        }
        PrivacyService.initSDKs()

        // 2. 拉取启动配置(静默失败)
        try? await AppStartService.fetchAndCache()

        // 3. 检查更新
        if let update = AppStartService.cachedConfig?.update, update.hasUpdate {
            if update.forceUpdate {
                coordinator.goForceUpdate(update); return  // 永久挂起
            }
            // 可选更新:弹窗,用户选择跳过后继续
            await coordinator.showOptionalUpdate(update)
        }

        // 4. 验证登录态
        await navigateByToken(coordinator: coordinator)
    }

    private func navigateByToken(coordinator: AppCoordinator) async {
        guard let _ = AppStorage.getString(StorageKeys.accessToken) else {
            coordinator.goLogin(); return
        }
        await AuthService.shared.reLoginIM()
        coordinator.goMain()
    }
}
```

---

## 十一、登录注册

### 11.1 数据模型

```swift
struct SendSmsResponse: Decodable {
    let expireSeconds: Int
    let retryAfterSeconds: Int        // 倒计时秒数(默认60)
}

struct LoginResponse: Decodable {
    let isNewUser: Bool
    let accessToken: String
    let refreshToken: String
    let accessExpiresIn: Int
    let refreshExpiresIn: Int
    let imToken: String
    let user: LoginUser
}

struct LoginUser: Decodable, Sendable {
    let uid: Int
    var nickname: String
    var avatar: String
    var gender: Int                   // 0=未设置 1=男 2=女
    var isVerified: Bool
    var isVip: Bool
    var bio: String
    var birthDate: String             // "yyyy-MM-dd"
    var mbti: String
    var residence: Residence?
    var orientation: Int
    var attribute: Int
    var isPersonalized: Bool
    var isProfileHidden: Bool
}

struct Residence: Codable, Sendable {
    var countryName: String
    var provinceName: String
    var cityName: String
    var cityCode: String
}
```

### 11.2 AuthService(Swift 6 Actor 版)

```swift
// AuthService.swift
actor AuthService {
    static let shared = AuthService()

    func login(phone: String, code: String? = nil, password: String? = nil) async throws {
        let deviceInfo = await DeviceInfo.current      // @MainActor 属性需要 await
        let request = PhoneLoginRequest(phone: phone, code: code,
                                        password: password, deviceInfo: deviceInfo)
        let resp = try await APIClient.shared.post("/auth/login/phone",
                                                    body: request, as: LoginResponse.self)
        // 1. 持久化
        AppStorage.setString(StorageKeys.accessToken, resp.accessToken)
        AppStorage.setString(StorageKeys.refreshToken, resp.refreshToken)
        AppStorage.setString(StorageKeys.userId, String(resp.user.uid))
        AppStorage.setString(StorageKeys.imToken, resp.imToken)
        if let json = try? JSONEncoder().encode(resp.user),
           let str = String(data: json, encoding: .utf8) {
            AppStorage.setString(StorageKeys.userProfile, str)
        }

        // 2. IM 登录
        try await IMAdapter.shared.login(uid: String(resp.user.uid), token: resp.imToken)

        // 3. 媒体缓存初始化
        await MediaCacheService.shared.initialize(userId: String(resp.user.uid))

        // 4. 路由(切回 MainActor)
        await MainActor.run {
            if resp.isNewUser {
                AppCoordinator.shared.goCompleteProfile()
            } else {
                AppCoordinator.shared.goMain()
            }
        }
    }

    func logout() async {
        let deviceId = AppStorage.getString(StorageKeys.deviceId) ?? ""
        try? await APIClient.shared.post("/auth/logout", body: ["device_id": deviceId])
        await IMAdapter.shared.logout()
        clearLocalAuth()
        await MainActor.run { AppCoordinator.shared.goLogin() }
    }

    func reLoginIM() async {
        guard let uid = AppStorage.getString(StorageKeys.userId),
              let imToken = AppStorage.getString(StorageKeys.imToken) else { return }
        do {
            try await IMAdapter.shared.login(uid: uid, token: imToken)
        } catch {
            await silentRefreshImToken()
        }
    }

    func silentRefreshImToken() async {
        guard let resp = try? await APIClient.shared.get("/auth/im/token",
                                                         as: ImTokenResponse.self),
              let uid = AppStorage.getString(StorageKeys.userId) else { return }
        AppStorage.setString(StorageKeys.imToken, resp.imToken)
        try? await IMAdapter.shared.login(uid: uid, token: resp.imToken)
    }

    nonisolated static func forceLogout() {
        [StorageKeys.accessToken, StorageKeys.refreshToken,
         StorageKeys.userId, StorageKeys.imToken].forEach { AppStorage.remove($0) }
        Task { await IMAdapter.shared.logout() }
        Task { @MainActor in AppCoordinator.shared.goLogin() }
    }

    private func clearLocalAuth() {
        [StorageKeys.accessToken, StorageKeys.refreshToken,
         StorageKeys.userId, StorageKeys.imToken, StorageKeys.userProfile]
            .forEach { AppStorage.remove($0) }
    }
}
```

### 11.3 LoginView(SwiftUI + Liquid Glass)

```swift
struct LoginView: View {
    @State private var viewModel = AuthViewModel()
    @State private var mode: LoginMode = .sms
    @State private var phone = ""
    @State private var code = ""
    @State private var password = ""
    @State private var countdown = 0

    enum LoginMode { case sms, password }

    var body: some View {
        ScrollView {
            VStack(spacing: 0) {
                // 头部 Logo
                VStack(spacing: 12) {
                    Image(systemName: "compass.drawing")
                        .font(.system(size: 56))
                        .foregroundStyle(.accent)
                    Text("同伴").font(.largeTitle.bold())
                    Text("遇见同路人,分享精彩时刻")
                        .font(.caption).foregroundStyle(.secondary)
                }
                .padding(.top, 60).padding(.bottom, 48)

                // 模式切换(Liquid Glass Segmented)
                Picker("登录方式", selection: $mode) {
                    Text("验证码登录").tag(LoginMode.sms)
                    Text("密码登录").tag(LoginMode.password)
                }
                .pickerStyle(.segmented)
                .padding(.bottom, 24)

                // 输入区
                VStack(spacing: 16) {
                    TextField("手机号", text: $phone)
                        .keyboardType(.phonePad)
                        .textFieldStyle(.roundedBorder)

                    if mode == .sms {
                        HStack {
                            TextField("6位验证码", text: $code)
                                .keyboardType(.numberPad)
                                .textFieldStyle(.roundedBorder)
                            Button(countdown > 0 ? "\(countdown)s" : "获取验证码") {
                                Task { await sendCode() }
                            }
                            .disabled(countdown > 0 || phone.count != 11)
                            .glassEffect(.regular.interactive())
                        }
                    } else {
                        SecureField("密码", text: $password)
                            .textFieldStyle(.roundedBorder)
                    }
                }

                // 登录按钮(Liquid Glass 强调色)
                Button {
                    Task { await login() }
                } label: {
                    Group {
                        if viewModel.isLoading {
                            ProgressView().tint(.white)
                        } else {
                            Text("立即登录").fontWeight(.semibold)
                        }
                    }
                    .frame(maxWidth: .infinity).frame(height: 50)
                }
                .glassEffect(.regular.tint(.accent).interactive())
                .padding(.top, 32)
                .disabled(viewModel.isLoading)

                // 协议
                HStack(spacing: 2) {
                    Text("登录即代表同意").font(.caption2).foregroundStyle(.secondary)
                    Button("《用户协议》") { }.font(.caption2)
                    Text("与").font(.caption2).foregroundStyle(.secondary)
                    Button("《隐私政策》") { }.font(.caption2)
                }
                .padding(.top, 16)
            }
            .padding(.horizontal, 24)
        }
    }

    private func sendCode() async {
        guard phone.count == 11 else { return }
        do {
            let resp = try await viewModel.sendSms(phone: phone)
            startCountdown(resp.retryAfterSeconds)
        } catch { }
    }

    private func login() async {
        do {
            try await viewModel.login(phone: phone,
                                      code: mode == .sms ? code : nil,
                                      password: mode == .password ? password : nil)
        } catch { }
    }
}
```

### 11.4 完善资料(三步 SwiftUI)

```swift
struct CompleteProfileView: View {
    @State private var step = 0
    @State private var viewModel = CompleteProfileViewModel()

    var body: some View {
        VStack(spacing: 0) {
            // 进度条
            HStack(spacing: 4) {
                ForEach(0..<3) { i in
                    RoundedRectangle(cornerRadius: 2)
                        .fill(i <= step ? Color.accentColor : Color.secondary.opacity(0.3))
                        .frame(height: 3)
                }
            }.padding(.horizontal)

            // 步骤内容(TabView 模拟 PageViewController)
            TabView(selection: $step) {
                Step0View(viewModel: viewModel, onNext: { step = 1 }).tag(0)
                Step1View(viewModel: viewModel, onNext: { step = 2 }).tag(1)
                Step2View(viewModel: viewModel, onFinish: { Task { await viewModel.finish() }}).tag(2)
            }
            .tabViewStyle(.page(indexDisplayMode: .never))
            .animation(.easeInOut, value: step)
        }
        .toolbar {
            // Step 2 右上角跳过
            if step == 2 {
                ToolbarItem(placement: .topBarTrailing) {
                    Button("跳过") { Task { await viewModel.finish() } }
                }
            }
        }
    }
}
```

**三步内容(对应 Flutter 逻辑):**
- **Step 0(必填):** PHPickerViewController 选图 → TOCropViewController 裁剪 → 上传 OSS → 昵称(≥2字)
- **Step 1(必填):** 性别(两张卡片)+ 生日(DatePicker sheet)+ 常住地(省市级联 sheet)
- **Step 2(可跳过):** 性取向(Grid)+ 角色属性(有条件显示,动画展开)

---

## 十二、AppDelegate 初始化

```swift
class AppDelegate: NSObject, UIApplicationDelegate {

    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool {
        // 1. MMKV(最先)
        MMKV.initialize(rootDir: nil)

        // 2. Keychain DeviceId
        if KeychainHelper.read("device_id") == nil {
            KeychainHelper.write("device_id", UUID().uuidString)
        }

        // 3. 日志
        AppLogger.configure()

        // 4. 安全检测
        SecurityAdapter.shared.runChecks()

        // 5. 微信 SDK 注册(仅注册 URL Scheme,不初始化跟踪)
        WXApi.registerApp(AppConfig.wechatAppId, universalLink: AppConfig.wechatUniversalLink)

        // 6. OpenIM 配置(不登录,登录在 AuthService.reLoginIM 中)
        IMAdapter.shared.configure()

        return true
    }

    // 微信/支付宝 URL 回调
    func application(_ app: UIApplication, open url: URL,
                     options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
        WXApi.handleOpen(url, delegate: WechatAdapter.shared)
        AlipaySDK.defaultService().processOrder(withPaymentResult: url) { _ in }
        return true
    }

    func application(_ application: UIApplication,
                     continue userActivity: NSUserActivity,
                     restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        WXApi.handleOpenUniversalLink(userActivity, delegate: WechatAdapter.shared)
    }
}
```

---

## 十三、环境配置(Xcode xcconfig)

```
// Config/Debug.xcconfig
API_BASE_URL = https://api-dev.tongban.com
APP_KEY = dev_key_xxx
WECHAT_APP_ID = wx_dev_xxxx

// Config/Release.xcconfig
API_BASE_URL = https://api.tongban.com
APP_KEY = prod_key_xxx
WECHAT_APP_ID = wx_prod_xxxx
```

```swift
enum AppConfig {
    static let apiBaseURL     = Bundle.main.infoDictionary!["API_BASE_URL"] as! String
    static let appKey         = Bundle.main.infoDictionary!["APP_KEY"] as! String
    static let wechatAppId    = Bundle.main.infoDictionary!["WECHAT_APP_ID"] as! String
    static let wechatUniversalLink = "https://tongban.com/app/"
}
```

---

## 十四、Liquid Glass 使用原则(项目规范)

| 场景 | 做法 |
|---|---|
| 全局 Tab 栏 | `TabView` 自动,加 `.tabBarMinimizeBehavior(.onScrollDown)` |
| 导航栏 | `NavigationStack` 自动,无需额外配置 |
| 悬浮操作按钮 | `.glassEffect(.regular.interactive())` |
| 底部悬浮操作栏 | `GlassEffectContainer` 包裹多个按钮,共享融合效果 |
| 弹层/Sheet | `sheet()` 系统自动 Liquid Glass |
| 普通卡片列表 | **不用** glassEffect,用 `.background(.background.secondary)` |
| 强调色按钮(登录/发射信号) | `.glassEffect(.regular.tint(.accent).interactive())` |
| 毛玻璃蒙层 | `.glassEffect(.clear)` + 暗色蒙层叠加 |
| 视频/全图全屏 | **不加任何 glass** |

---

## 十五、搭建顺序

1. **Xcode 项目 + Podfile** — `platform :ios, '26.0'`,安装依赖
2. **Core/Storage** — MMKV 封装、StorageKeys、Keychain DeviceId
3. **Core/Network** — APIClient + 三个拦截器
4. **AppDelegate + TongbanApp.swift** — @main 入口,MMKV 初始化
5. **Theme** — AppColors / AppFonts / AppSpacing
6. **AppCoordinator** — 路由状态机(各 View 先用 Text 占位)
7. **SplashView** — 启动状态机(含超时兜底)
8. **PrivacyView** — 隐私协议门槛
9. **LoginView + AuthService** — 完整登录链路跑通(含 IM 登录)
10. **CompleteProfileView** — 新用户三步引导
11. **MainTabView** — 三 Tab 骨架(Liquid Glass 自动生效)
12. **TokenInterceptor** — Token 自动刷新
13. **IMAdapter** — OpenIM 完整接入
14. **Signal 模块** — 收件箱 → 发布 → 详情
15. **Chat 模块** — 会话列表 → 私聊