架构
一份状态,别存两遍
加载中、失败、列表内容应该在同一个类型里。派生值不要再写进第二份存储。
界面出错,很多时候不是绘制写错,而是两份状态不一致:isLoading 还是 true,error 也有值,items 里留着上一页的数据。用户看见的是上一页内容加上一个错误弹窗,再加一个转圈。三个标志位可以组合出八种情况,其中一半没有意义。
用一个阶段表示
加载、空、内容、失败,是互斥的阶段,适合写成枚举。列表数据只在内容阶段存在,失败时不要假装还有一份“也许能显示”的旧数组,除非产品明确要求保留旧数据并提示刷新失败。那种需求也要写成明确的状态,例如 .ready(items:errorBanner:),而不是三个独立布尔值。
enum Page<Value> {
case loading
case empty
case content(Value)
case failed(message: String)
}
extension Page {
var items: Value? {
if case .content(let value) = self { return value }
return nil
}
}
能算出来的不要存。行数是 items.count。按钮是否可点是阶段不是 loading。再存一个 canSubmit,下次有人只改了阶段,按钮就和真实状态分家。
观察的粒度
ObservableObject 把整个对象的 objectWillChange 发出去,任何一个属性变化,订阅的界面都可能刷新。字段多的时候,把会高频变的进度和很少变的配置拆开。SwiftUI 的 @Observable 按属性跟踪,粒度细一些,但在视图身份被频繁销毁时,状态要放在不会跟着视图一起死的地方,否则滚动位置和输入会丢。
不要在 View 的 body 或 viewDidLayoutSubviews 里改状态。那会形成“绘制触发状态变化,状态变化又触发布局”的环。事件来自用户操作、网络回调、计时器,布局只读状态。
错误也是状态
网络错误如果只 present 一个 alert,旋转或重新进入页面就没了,调用方也不知道能不能重试。把错误放进状态,界面决定用横幅、空页还是提示。埋点在副作用边缘记一次即可,不要每个弹窗再记一遍,否则重试会把同一条失败上报多次。