在 SwiftUI 和 Combine 的响应式世界中,状态(State)是流动的。状态不一致(State Inconsistency) 通常源于“多个数据源(Single Source of Truth 缺失)”或“非法状态组合”。
通过防御式编程,我们可以将逻辑约束注入到声明式代码中。
1. 使用枚举建模状态(消除非法状态)
防御式编程的第一步是使非法状态无法表达。避免使用多个互相独立的 Bool(如 isLoading, hasError, data),因为它们可能组合出逻辑矛盾(例如同时为 loading 和 hasData)。
- 实践:使用
enum封装页面状态。 - 防御点:
switch语句的详尽性检查强制你处理所有情况,不会遗漏错误状态。
2. 单向数据流 (UDF) 与逻辑封装
在 SwiftUI 中,防御式编程意味着限制对状态的修改权限。
- 实践:在
ViewModel中,将@Published属性设为private(set)。 - 防御点:确保外部视图只能读取状态,而不能随意修改。所有修改必须通过定义的
Action函数进入,这形成了一个可预测的“漏斗”,便于拦截非法操作。
Swift
final class UserViewModel: ObservableObject {
@Published private(set) var state: ViewState = .idle
func dispatch(_ action: Action) {
// 防御检查:如果正在加载,则忽略重复的请求
guard case .loading = state else { return }
// 执行逻辑...
}
}
3. Combine 管道中的防御性算子
在处理数据流时,Combine 提供了强大的工具来防止“无效数据”进入 UI 层:
.removeDuplicates():防御性地防止无意义的 UI 重绘。.replaceError(with:):确保即使上游崩溃,下游 UI 也能拿到一个安全的默认值,而不是直接挂起。.handleEvents(receiveTermination:):在流意外结束时记录日志,防止静默失败。
4. 属性包装器 (Property Wrappers) 进行数据校验
你可以创建自定义的属性包装器来充当“门卫”,确保赋值永远在合法范围内。
- 实践:创建一个
@Clamped或@Trimmed包装器。 - 防御点:在数据到达 View 之前,自动修正(而非报错)非法输入。
Swift
@propertyWrapper
struct Clamped<Value: Comparable> {
var value: Value
let range: ClosedRange<Value>
var wrappedValue: Value {
get { value }
set { value = min(max(newValue, range.lowerBound), range.upperBound) }
}
}
// 使用:确保进度条永远在 0~1 之间
@Clamped(range: 0...1) var progress: Double = 0
5. 防御性 View 构建:避免非预期的布局
SwiftUI 的布局引擎非常灵活,但也容易产生意料之外的压缩或拉伸。
.fixedSize()和.layoutPriority():显式声明哪些组件是不允许被压缩的。ContentUnavailableView(iOS 17+) :防御性地处理空数据场景,提供统一的降级 UI。
总结:防御性状态检查表
| 风险点 | 防御方案 |
|---|---|
| 逻辑状态矛盾 | 使用 enum 替代多个 Bool |
| 外部非法修改状态 | private(set) 配合明确的 Action |
| 重复或频繁触发请求 | Combine 的 debounce 或 removeDuplicates |
| 非预期 nil 数据 | guard let 配合 SwiftUI 的 if let 视图展开 |
| 网络数据异常 | 在 Decodable 映射阶段进行严格校验 |