提示信息
# 同伴 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 模块** — 会话列表 → 私聊