应用门控

启动体验:错误边界、生物识别锁、更新、引导页、隐私同意。

在主外壳渲染之前,应用会穿过一叠门控(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 页 TabViewOnboardingView),含跳过 / 下一步 / 页点,直到 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 封装)读写,因此新增你自己的门控遵循同一范式:一个 持久化标志 + 一个条件视图。

相关页面

On this page