应用门控
启动体验:错误边界、生物识别锁、更新、引导页、隐私同意。
在主外壳渲染之前,应用会穿过一叠门控(gate)——每个都是可组合视图,各自决定是展示子内容,还是用自己的
UI 打断。这就是启动体验:崩溃恢复、应用锁、更新提示、引导页与法律同意,顺序固定。每个都是 Integrated,
由 AppPreferences + AppSecrets 驱动。
门控栈
RootView 由外到内嵌套门控——最外层最先运行:
ErrorBoundary { // 全局崩溃/错误兜底
BiometricGate(initialLocked: …) { // Face ID / Touch ID 应用锁
UpdateGate { // 强制 / 软更新提示
OnboardingGate { // 首次运行的引导幻灯片
ConsentGate { // 法律同意 → 启动分析
AuthGate() // 登录 vs 主外壳
}
}
}
}
}每个门控读取已持久化的状态,因此回访用户会径直落到 AuthGate。评分提示不是门控——它是从应用内部触发的
智能触发器(见下文)。
关键文件
错误边界
ErrorBoundary 包住一切(挂在 BiometricGate 之外,使错误始终能浮现)。视图模型把不可恢复的错误 post 到
全局的 AppErrorBus.shared.post(...)——无需 DI 穿线——边界随即覆盖一张全屏“出错了”卡片,含重试
(清空总线)与发送报告(提交 Sentry 用户反馈)。堆栈细节仅在 DEBUG 渲染。见
可观测性。
生物识别锁
当 AppPreferences.biometricEnabled 打开时(在“账户安全”里切换),BiometricGate 把应用锁在 Face ID /
Touch ID / Optic ID 之后。行为:
- 启用状态下首次启动 → 锁定;解锁运行
LAContext.evaluatePolicy。 - 进入后台会打时间戳;超过 1 秒宽限期后再激活会重新锁定并重新提示。
- 当
scenePhase != .active时,一张不透明的隐私遮罩把内容从多任务快照中挡住。 - 取消会保持锁定;真正的会话密钥存放在钥匙串(此开关只控制是否锁定)。
更新门控
UpdateController 把运行构建的 CFBundleShortVersionString 与两个密钥比较,驱动 UpdateGate:
| 密钥 | 效果 |
|---|---|
MIN_APP_VERSION | 低于它 → 强制更新:不可关闭的底部弹窗 |
LATEST_APP_VERSION | 低于它 → 软更新:可关闭的弹窗,按版本持久化关闭状态 |
两个动作都在 Safari 打开 APP_STORE_URL(iOS 没有应用内更新 API)。版本比较是逐段数值比较
(1.0 < 1.0.1 < 1.2)。
无需发版即可本地测试:在 Secrets.xcconfig 中把 LATEST_APP_VERSION(软)或 MIN_APP_VERSION
(强制)调到高于当前版本并重启。在 DEBUG 下,TestView 也能直接强制某个门控状态。
引导页
OnboardingGate 展示一个 4 页 TabView(OnboardingView),含跳过 / 下一步 / 页点,直到
AppPreferences.onboardingCompleted 被置位。完成会触发 onboarding completed 分析事件并记一次评分动作。
它不自动跳到权限界面(那是从 Me 页选择进入的)。
隐私同意
ConsentGate 阻断其下所有内容,直到
AppPreferences.acceptedConsentVersion == LegalContent.version(一个带日期的版本串,如 2026-04-30——
提升它会向所有人重新提示)。它内联展示打包的法律文档,并且关键地仅在接受后才启动分析:
// ConsentGate —— 分析在同意之前绝不触发
content.task { env.analytics.start() }打包的条款 / 隐私 / EULA 从本地内容渲染;“在线查看”链接在设置时使用 TERMS_URL / PRIVACY_URL /
EULA_URL。把同意置于分析启动之上,正是让应用可安全通过 App 审核的关键。
评分(智能触发,非门控)
RatingController.recordAction(_:) 仅当三者全满足时才经 AppStore.requestReview(in:) 请求应用内评分:
自上次提示以来记录了 ≥ 3 次动作、当前应用版本尚未提示过、距上次提示 ≥ 90 天。从自然的成功时刻调用它
(如引导完成、清空一条 todo)。Apple 另会把提示上限设为每 365 天 3 次,并可能抑制该 UI。
定制这叠门控
- 通过编辑
RootView的嵌套来重排 / 移除门控——例如不想要应用锁就去掉BiometricGate,或把UpdateGate移到BiometricGate之上。每个门控自成一体,读取各自的状态。 - 让驱动状态/密钥留空以在运行时禁用门控:引导完成后隐藏、同意后隐藏、无
MIN/LATEST_APP_VERSION时更新保持 idle、开关关闭时生物识别保持开放。 - 门控通过
AppPreferences(一个UserDefaults封装)读写,因此新增你自己的门控遵循同一范式:一个 持久化标志 + 一个条件视图。
