12-14.【SwiftUI】SwiftUI 中,如何设计 State 以保证 UI 可预测、易调试、易回滚?

29 阅读4分钟

在 SwiftUI 设计状态(State)时,目标是实现**“数据确定性” 。要达到可预测、易调试、易回滚,核心策略是采用单向数据流(UDF)状态快照化**。

以下是具体的架构设计指南:

1. 核心设计:单一事实来源(SSOT)与状态快照

原则: 将所有的局部变量收拢到一个单一的结构体(Struct)中。

  • 易回滚: 因为 State 是值类型(Struct),你可以通过简单的赋值 self.state = backupState 实现完美回滚。
  • 易调试: 只需要打印这一个结构体,就能获取当前 UI 的完整静态描述。

Swift

struct MyPageState: Equatable {
    var items: [String] = []
    var isLoading: Bool = false
    var error: String? = nil
    
    // 支持“撤销”或“回滚”的快照机制
    static let initial = MyPageState()
}

2. 状态转换的原子性(The Action Pattern)

不要在 View 或 ViewModel 中散乱地修改状态。通过**“意图(Action)”**来驱动转换。

  • 可预测: 所有的状态变化都发生在一个闭包或函数内,你可以清晰地看到“输入 Action -> 输出新 State”的过程。

Swift

@MainActor
class MyViewModel: ObservableObject {
    @Published private(set) var state: MyPageState = .initial
    private var history: [MyPageState] = [] // 记录历史用于回滚

    func send(_ action: Action) {
        // 在修改前保存快照
        history.append(state)
        
        switch action {
        case .fetchStarted:
            state.isLoading = true
        case .fetchSuccess(let items):
            state.isLoading = false
            state.items = items
        case .undo:
            if let previous = history.popLast() {
                state = previous
            }
        }
    }
}

3. 利用枚举处理互斥状态(Enum for Mutually Exclusive States)

避免使用多个互相竞争的布尔值(如 isLoadingshowError)。

  • 避免非法状态: 枚举从物理上保证了 App 不可能同时处于“正在加载”和“显示成功数据”的状态。

Swift

enum ViewStatus: Equatable {
    case idle
    case loading
    case success([Item])
    case failure(String)
}

struct MyState {
    var status: ViewStatus = .idle
}

4. 调试与回滚的防御式技巧

A. 状态审计日志 (State Auditing)

利用 didSetCombinehandleEvents 自动记录状态变化的轨迹。

Swift

@Published var state: MyPageState = .initial {
    didSet {
        print("状态变更轨迹: (oldValue) -> (state)")
    }
}
B. 利用 Transaction 实现 UI 回滚动画

当进行状态回滚时,使用 withAnimationTransaction 确保 UI 的变化是平滑且受控的。

Swift

func rollback() {
    var transaction = Transaction(animation: .spring())
    transaction.disablesAnimations = false 
    withTransaction(transaction) {
        self.state = history.popLast() ?? .initial
    }
}

5. 易测试性的保证

由于 State 是一个遵循 Equatable 的值类型,你的单元测试会变得极其简单:

Swift

func test_fetchSuccess_updatesState() {
    let viewModel = MyViewModel()
    viewModel.send(.fetchSuccess(["A", "B"]))
    
    // 只需要对比两个值,不需要繁琐的 UI 检查
    XCTAssertEqual(viewModel.state.items, ["A", "B"])
    XCTAssertFalse(viewModel.state.isLoading)
}

总结清单

  1. 收拢状态:用一个 Struct 包装所有字段。
  2. 枚举互斥:用 Enum 代替零散的 Bool。
  3. 单向流动:通过统一的 send()dispatch() 函数修改状态。
  4. 快照留痕:保留 history 数组以实现秒级回滚。

12-14. 【SwiftUI】How to Design State to Ensure UI Predictability, Debuggability, and Easy Rollbacks?

When designing State in SwiftUI, the goal is to achieve "Data Determinism." To make an app predictable, easy to debug, and simple to roll back, the core strategy involves adopting Unidirectional Data Flow (UDF) and State Snapshotting.

Here is a specific architectural design guide:


1. Core Design: Single Source of Truth (SSOT) and State Snapshots

Principle: Consolidate all local variables into a single struct.

  • Easy Rollbacks: Because State is a value type (struct), you can achieve a perfect rollback simply by assigning self.state = backupState.
  • Easy Debugging: You only need to print this one struct to obtain a complete static description of the current UI.

Swift

struct MyPageState: Equatable {
    var items: [String] = []
    var isLoading: Bool = false
    var error: String? = nil
    
    // Snapshot mechanism supporting "Undo" or "Rollback"
    static let initial = MyPageState()
}

2. Atomicity of State Transitions (The Action Pattern)

Do not scatter state modifications throughout your View or ViewModel. Drive transitions through an "Intent (Action)."

  • Predictable: All state changes occur within a single closure or function. You can clearly observe the process of "Input Action -> Output New State."

Swift

@MainActor
class MyViewModel: ObservableObject {
    @Published private(set) var state: MyPageState = .initial
    private var history: [MyPageState] = [] // Record history for rollbacks

    func send(_ action: Action) {
        // Save a snapshot before modification
        history.append(state)
        
        switch action {
        case .fetchStarted:
            state.isLoading = true
        case .fetchSuccess(let items):
            state.isLoading = false
            state.items = items
        case .undo:
            if let previous = history.popLast() {
                state = previous
            }
        }
    }
}

3. Use Enums for Mutually Exclusive States

Avoid using multiple competing Booleans (e.g., isLoading and showError).

  • Avoid Illegal States: Enums physically guarantee that the app cannot be in both a "Loading" and "Success" state simultaneously.

Swift

enum ViewStatus: Equatable {
    case idle
    case loading
    case success([Item])
    case failure(String)
}

struct MyState {
    var status: ViewStatus = .idle
}

4. Defensive Techniques for Debugging and Rollbacks

A. State Auditing Logs

Use didSet or Combine’s handleEvents to automatically track the trajectory of state changes.

Swift

@Published var state: MyPageState = .initial {
    didSet {
        print("State Change Track: (oldValue) -> (state)")
    }
}
B. Using Transaction for UI Rollback Animations

When rolling back state, use withAnimation and Transaction to ensure the UI transition is smooth and controlled.

Swift

func rollback() {
    var transaction = Transaction(animation: .spring())
    transaction.disablesAnimations = false 
    withTransaction(transaction) {
        self.state = history.popLast() ?? .initial
    }
}

5. Guarantees for Testability

Since your State is a value type conforming to Equatable, unit testing becomes trivial:

Swift

func test_fetchSuccess_updatesState() {
    let viewModel = MyViewModel()
    viewModel.send(.fetchSuccess(["A", "B"]))
    
    // Only need to compare two values; no complex UI inspection required
    XCTAssertEqual(viewModel.state.items, ["A", "B"])
    XCTAssertFalse(viewModel.state.isLoading)
}

Summary Checklist

  1. Consolidate State: Wrap all fields in a single Struct.
  2. Enum Exclusivity: Use Enums instead of scattered Booleans.
  3. Unidirectional Flow: Modify state only through a unified send() or dispatch() function.
  4. Snapshot History: Maintain a history array to implement instant rollbacks.