ImmutableData 0.1.0

SwiftUI 应用的简单状态管理

背景

凭借十多年来使用声明式 UI 框架大规模交付产品的经验,我们为 SwiftUI 提出了一种新的应用程序架构。 以 Flux 和 Redux 架构作为哲学上的先例,我们可以使用现代 Swift 并专门为现代 SwiftUI 设计一种架构。 这种架构鼓励声明式思维而不是命令式思维,函数式编程而不是面向对象编程,以及不可变模型值而不是可变模型对象。

该架构的核心是单向数据流

flowchart LR
  accTitle: Data Flow in the ImmutableData Framework
  accDescr: Data flows from action to state, and from state to view, in one direction only.
  A[Action] --> B[State] --> C[View]
加载

所有全局状态数据都按照这个基本模式流经应用程序,并且强制执行严格的关注点分离。 Actions 声明发生了什么,无论是用户输入、服务器响应还是设备传感器的变化,但它们对状态层或视图层一无所知。 状态层响应 action 描述的“新闻”,并相应地更新状态。 对状态进行更改的所有逻辑都包含在状态层中,但它对视图层一无所知。 然后,当新状态流经组件树时,视图响应状态层的变化。 然而,视图层对状态层一无所知。 通过保持这种严格的单向数据流和关注点分离,我们的应用程序代码变得更容易测试、更容易推理、更容易向新团队成员解释,并且在新功能需要时更容易更新。

此外,避免像双向数据绑定这样的复杂性,或者由可变性产生的意大利面条式代码,可以使我们的代码变得干净、快速且可维护。 这是此应用程序框架(及其背后的思想)与之前在 WWDC 上展示的其他 actions、状态和视图之间的关键区别。1 通过避免从状态层外部调用的直接突变,并拥抱不可变性,复杂性就会消失,我们的代码也会变得更加健壮。

ImmutableData 编程指南 是学习 ImmutableData 的权威参考。

要求

ImmutableData 基础架构部署到以下平台

构建 ImmutableData 包需要 Xcode 16.0+ 和 macOS 14.5+。

如果您遇到任何兼容性问题,请提交 GitHub issue。

用法

ImmutableData 包为您的产品提供了三个库模块

ImmutableData 编程指南提供了完整的示例应用程序产品教程。 这是学习如何使用 ImmutableData 构建产品的推荐方法。

一个非常基本的“Hello World”应用程序将是一个计数器:一个 SwiftUI 应用程序,用于递增和递减一个整数值。

我们从计数器应用程序的数据模型开始

typealias CounterState = Int

enum CounterAction {
  case didTapIncrementButton
  case didTapDecrementButton
}

enum CounterReducer {
  @Sendable static func reduce(
    state: CounterState,
    action: CounterAction
  ) -> CounterState {
    switch action {
    case .didTapIncrementButton:
      state + 1
    case .didTapDecrementButton:
      state - 1
    }
  }
}

我们定义一个 EnvironmentKey 值,它将保存一个 ImmutableData.Store 实例

import ImmutableData
import ImmutableUI
import SwiftUI

@MainActor struct StoreKey: @preconcurrency EnvironmentKey {
  static let defaultValue = ImmutableData.Store<CounterState, CounterAction>(
    initialState: 0,
    reducer: { state, action in
      fatalError("missing store")
    }
  )
}

extension EnvironmentValues {
  var store: ImmutableData.Store<CounterState, CounterAction> {
    get {
      self[StoreKey.self]
    }
    set {
      self[StoreKey.self] = newValue
    }
  }
}

StoreKey 返回的 defaultValue 使用一个 reducer 构建,该 reducer 会崩溃以指示程序员错误:我们的组件图应使用显式传递到环境的 stores。

我们现在可以构建一个 SwiftUI App

@main
@MainActor struct CounterApp {
  @State var store = ImmutableData.Store(
    initialState: 0,
    reducer: CounterReducer.reduce
  )
}

extension CounterApp: App {
  var body: some Scene {
    WindowGroup {
      ImmutableUI.Provider(
        \.store,
        self.store
      ) {
        CounterContent()
      }
    }
  }
}

这是我们的 CounterContent,用于显示和转换全局应用程序状态

@MainActor struct CounterContent {
  @ImmutableUI.Selector(
    \.store,
    outputSelector: OutputSelector(
      select: { $0 },
      didChange: { $0 != $1 }
    )
  ) var value

  @ImmutableUI.Dispatcher(\.store) var dispatcher
}

extension CounterContent: View {
  var body: some View {
    VStack {
      Button("Increment") {
        self.didTapIncrementButton()
      }
      Text("Value: \(self.value)")
      Button("Decrement") {
        self.didTapDecrementButton()
      }
    }
  }
}

这是可以分发的两个 action 值

extension CounterContent {
  func didTapIncrementButton() {
    do {
      try self.dispatcher.dispatch(action: .didTapIncrementButton)
    } catch {
      print(error)
    }
  }
}

extension CounterContent {
  func didTapDecrementButton() {
    do {
      try self.dispatcher.dispatch(action: .didTapDecrementButton)
    } catch {
      print(error)
    }
  }
}

SwiftUI 示例应用

Samples/Samples.xcworkspace 工作区包含三个从 ImmutableData 基础架构构建的示例应用程序产品。 ImmutableData 编程指南中讨论了这些产品。

许可

版权所有 2024 Rick van Voorden 和 Bill Fisher

根据 Apache 许可证 2.0 版(“许可证”)获得许可;除非符合许可证的规定,否则您不得使用此文件。 您可以在以下网址获取许可证副本:

https://apache.ac.cn/licenses/LICENSE-2.0

除非适用法律要求或以书面形式同意,否则根据“按原样”基础分发的软件,不附带任何形式的明示或暗示的保证或条件。 有关权限和限制的特定语言,请参阅许可证。

脚注

  1. https://developer.apple.com/videos/play/wwdc2019/226