该库的灵感来源于 TCA (swift-composable-architecture),它允许您将状态更改逻辑从 SwiftUI 的 View 和 ObservableObject 中分离出来,并将其限制在 Reducer 中。
在 TCA 中,将子域集成到父域中会导致更高的计算成本,尤其是在应用程序的叶节点上。我们的库通过避免将子域集成到父域中来解决这个问题,从而消除了不必要的计算开销。为了与深度嵌套的视图共享值或逻辑,我们利用 SwiftUI 的 EnvironmentObject 属性包装器。这允许您无缝地编写可以在整个应用程序中访问的逻辑或状态。此外,我们的库简化了应用程序的构建过程。您不再需要记住各种 TCA 修饰符或自定义视图,例如 ForEachStore、IfLetStore、SwitchStore、sheet(store:) 等。
我们在此库中提供了示例实现。目前,我们仅提供了一个简单的 GitHub 存储库搜索应用程序,但我们计划在未来扩展更多示例。
主要功能的文档可在此处找到
let package = Package(
name: "YourProject",
...
dependencies: [
.package(url: "https://github.com/Ryu0118/swiftui-simplex-architecture", exact: "0.9.0")
],
targets: [
.target(
name: "YourTarget",
dependencies: [
.product(name: "SimplexArchitecture", package: "swiftui-simplex-architecture"),
]
)
]
)
用法几乎与 TCA 相同。状态定义使用 SwiftUI 中使用的属性包装器,例如 @State
、@Binding
、@FocusState
。
@Reducer
struct MyReducer {
enum ViewAction {
case increment
case decrement
}
func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<Self> {
switch action {
case .increment:
state.counter += 1
return .none
case .decrement:
state.counter -= 1
return .none
}
}
}
@ViewState
struct MyView: View {
@State var counter = 0
let store: Store<MyReducer> = Store(reducer: MyReducer())
var body: some View {
VStack {
Text("\(counter)")
Button("+") {
send(.increment)
}
Button("-") {
send(.decrement)
}
}
}
}
来自 View 的事件使用 ViewAction 定义。应保持私有或仅在 Reducer 中使用的操作应使用 ReducerAction。
如果存在您不想向 View 公开的 Actions,则 ReducerAction 非常有效。这是一个示例代码
@Reducer
struct MyReducer {
enum ViewAction {
case login
}
enum ReducerAction {
case loginResponse(TaskResult<Response>)
}
@Dependency(\.authClient) var authClient
func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<Self> {
switch action {
case .login:
return .run { [email = state.email, password = state.password] send in
await send(
.loginResponse(
TaskResult { try await authClient.login(email, password) }
)
)
}
case let .loginResponse(result):
...
return .none
}
}
}
@ViewState
struct MyView: View {
@State var email: String = ""
@State var password: String = ""
let store: Store<MyReducer>
...
}
如果您只想将状态保留在 Reducer 中,请使用 ReducerState。ReducerState 还可以有效地提高性能,因为即使值发生更改,也不会更新 View。
这是一个示例代码
@Reducer
struct MyReducer {
enum ViewAction {
case increment
case decrement
}
struct ReducerState {
var totalCalledCount = 0
}
func reduce(into state: StateContainer<MyView>, action: Action) -> SideEffect<Self> {
state.reducerState.totalCalledCount += 1
switch action {
case .increment:
if state.reducerState.totalCalledCount < 10 {
state.counter += 1
}
return .none
case .decrement:
state.counter -= 1
return .none
}
}
}
@ViewState
struct MyView: View {
...
init() {
store = Store(reducer: MyReducer(), initialReducerState: MyReducer.ReducerState())
}
...
}
如果您想将子 Reducer 的 Action 发送到父 Reducer,请使用 pullback。这是一个示例代码。
@ViewState
struct ParentView: View {
let store: Store<ParentReducer> = Store(reducer: ParentReducer())
var body: some View {
ChildView()
.pullback(to: /ParentReducer.Action.child, parent: self)
}
}
@Reducer
struct ParentReducer {
enum ViewAction {
}
enum ReducerAction {
case child(ChildReducer.Action)
}
func reduce(into state: StateContainer<ParentView>, action: Action) -> SideEffect<Self> {
switch action {
case .child(.onButtonTapped):
// do something
return .none
}
}
}
此库中有两个宏
@Reducer
@ViewState
@Reducer
是一个宏,它集成了 ViewAction
和 ReducerAction
以生成 Action
。
@Reducer
struct MyReducer {
enum ViewAction {
case loginButtonTapped
}
enum ReducerAction {
case loginResponse(TaskResult<User>)
}
// expand to ↓
enum Action {
case loginButtonTapped
case loginResponse(TaskResult<User>)
init(viewAction: ViewAction) {
switch viewAction {
case .loginButtonTapped:
self = .loginButtonTapped
}
}
init(reducerAction: ReducerAction) {
switch reducerAction {
case .loginResponse(let arg1):
self = .loginResponse(arg1)
}
}
}
...
}
Reducer.reduce(into:action:)
不再需要为两个操作 ViewAction
和 ReducerAction
准备,而是可以集成到 Action
中。
@ViewState
创建一个 ViewState
结构体,并使其符合 ActionSendable
协议。
@ViewState
struct MyView: View {
@State var counter = 0
let store: Store<MyReducer> = Store(reducer: MyReducer())
var body: some View {
VStack {
Text("\(counter)")
Button("+") {
send(.increment)
}
Button("-") {
send(.decrement)
}
}
}
// expand to ↓
struct ViewState: ViewStateProtocol {
var counter = 0
static let keyPathMap: [PartialKeyPath<ViewState>: PartialKeyPath<MyView>] = [\.counter: \.counter]
}
}
ViewState 结构体有两个主要目的
此外,通过符合 ActionSendable 协议,您可以将 Actions 发送到 Store。
对于测试,我们使用 TestStore。这需要 ViewState 结构体的实例,该实例由 @ViewState 宏生成。此外,我们将进行进一步的操作来断言当调度一个 action 时,它的行为如何演变。
@MainActor
func testReducer() async {
let store = MyView().testStore(viewState: .init())
}
我们需要始终证明状态按照我们期望的方式发生了改变。 例如,我们可以模拟用户点击递增和递减按钮的流程
@MainActor
func testReducer() async {
let store = MyView().testStore(viewState: .init())
await store.send(.increment) {
$0.count = 1
}
await store.send(.decrement) {
$0.count = 0
}
}
此外,当 effects 被步骤执行并且数据反馈到 store 时,有必要断言这些 effects。
@MainActor
func testReducer() async {
let store = MyView().testStore(viewState: .init())
await store.send(.fetchData)
await store.receive(\.fetchDataResponse.success) {
$0.data = ...
}
}
如果您正在使用 swift-dependencies,您可以按如下方式执行依赖注入
let store = MyView().testStore(viewState: .init()) {
$0.apiClient.fetchData = { _ in ... }
}