OneWay 是一个简单轻量级的状态管理库,使用单向数据流,完全兼容 Swift 6 并且构建于 Swift 并发之上。它的结构使其更容易始终保持线程安全。
它可以轻松地跨平台和框架集成,没有任何第三方依赖,允许您以其最纯粹的形式使用它。 OneWay 可以用于任何地方,不仅仅是展示层,来简化复杂的业务逻辑。如果您正在寻找实现单向逻辑的方法,OneWay 是一个直接且实用的解决方案。
当使用 Store
时,数据流如下所示。
在处理 UI 时,最好使用 ViewStore
以确保主线程操作。
在采纳 Reducer
协议之后,定义 Action
和 State
,然后在 reduce(state:action:)
函数中实现每个 Action
的逻辑。
struct CountingReducer: Reducer {
enum Action: Sendable {
case increment
case decrement
case twice
case setIsLoading(Bool)
}
struct State: Sendable, Equatable {
var number: Int
var isLoading: Bool
}
func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
switch action {
case .increment:
state.number += 1
return .none
case .decrement:
state.number -= 1
return .none
case .twice:
return .concat(
.just(.setIsLoading(true)),
.merge(
.just(.increment),
.just(.increment)
),
.just(.setIsLoading(false))
)
case .setIsLoading(let isLoading):
state.isLoading = isLoading
return .none
}
}
}
向 Store 发送一个 action 会导致通过 Reducer
改变 state
。
let store = Store(
reducer: CountingReducer(),
state: CountingReducer.State(number: 0)
)
await store.send(.increment)
await store.send(.decrement)
await store.send(.twice)
print(await store.state.number) // 2
ViewStore
的用法相同。但是,当在 MainActor
中工作时,例如在 UIViewController
或 View
的 body 中,可以省略 await
。
let store = ViewStore(
reducer: CountingReducer(),
state: CountingReducer.State(number: 0)
)
store.send(.increment)
store.send(.decrement)
store.send(.twice)
print(store.state.number) // 2
当状态发生变化时,您可以接收到一个新的状态。 它保证不会连续接收到相同的状态。
struct State: Sendable, Equatable {
var number: Int
}
// number <- 10, 10, 20 ,20
for await state in store.states {
print(state.number)
}
// Prints "10", "20"
当然,您也可以只观察特定的属性。
// number <- 10, 10, 20 ,20
for await number in store.states.number {
print(number)
}
// Prints "10", "20"
如果您想即使将相同的值分配给 State
仍继续接收该值,则可以使用 @Triggered
。 有关其他有用属性包装器(例如 @CopyOnWrite,@Ignored)的说明,请参考 此处。
struct State: Sendable, Equatable {
@Triggered var number: Int
}
// number <- 10, 10, 20 ,20
for await state in store.states {
print(state.number)
}
// Prints "10", "10", "20", "20"
当状态有多个属性时,状态可能会由于未订阅的其他属性而发生变化。 在这种情况下,如果您正在使用 AsyncAlgorithms,您可以按如下方式删除重复项。
struct State: Sendable, Equatable {
var number: Int
var text: String
}
// number <- 10
// text <- "a", "b", "c"
for await number in store.states.number {
print(number)
}
// Prints "10", "10", "10"
for await number in store.states.number.removeDuplicates() {
print(number)
}
// Prints "10"
它可以与 SwiftUI 无缝集成。
struct CounterView: View {
@StateObject private var store = ViewStore(
reducer: CountingReducer(),
state: CountingReducer.State(number: 0)
)
var body: some View {
VStack {
Text("\(store.state.number)")
Toggle(
"isLoading",
isOn: Binding<Bool>(
get: { store.state.isLoading },
set: { store.send(.setIsLoading($0)) }
)
)
}
.onAppear {
store.send(.increment)
}
}
}
还有一个辅助函数,可以轻松地创建 Binding。
struct CounterView: View {
@StateObject private var store = ViewStore(
reducer: CountingReducer(),
state: CountingReducer.State(number: 0)
)
var body: some View {
VStack {
Text("\(store.state.number)")
Toggle(
"isLoading",
isOn: store.binding(\.isLoading, send: { .setIsLoading($0) })
)
}
.onAppear {
store.send(.increment)
}
}
}
有关更多详细信息,请参阅示例。
您可以通过使用 cancellable()
使一个 effect 能够被取消。 并且您可以使用 cancel()
来取消一个可取消的 effect。
func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
switch action {
// ...
case .request:
return .single {
let result = await api.result()
return Action.response(result)
}
.cancellable("requestID")
case .cancel:
return .cancel("requestID")
// ...
}
}
您可以将任何符合 Hashable 协议的对象作为 effect 的标识符,而不仅仅是字符串。
enum EffectID {
case request
}
func reduce(state: inout State, action: Action) -> AnyEffect<Action> {
switch action {
// ...
case .request:
return .single {
let result = await api.result()
return Action.response(result)
}
.cancellable(EffectID.request)
case .cancel:
return .cancel(EffectID.request)
// ...
}
}
OneWay 支持各种 effects,例如 just
、concat
、merge
、single
、sequence
等。 有关更多详细信息,请参阅文档。
您可以通过实现 bind()
轻松接收外部状态。 如果 publishers 或 streams 中有需要重新绑定的更改,您可以调用 Store
的 reset()
。
let textPublisher = PassthroughSubject<String, Never>()
let numberPublisher = PassthroughSubject<Int, Never>()
struct CountingReducer: Reducer {
// ...
func bind() -> AnyEffect<Action> {
return .merge(
.sequence { send in
for await text in textPublisher.values {
send(Action.response(text))
}
},
.sequence { send in
for await number in numberPublisher.values {
send(Action.response(String(number)))
}
}
)
}
// ...
}
OneWay 提供了 expect
函数来帮助您编写简洁明了的测试。 此函数异步工作,允许您验证状态是否按预期更新。
在使用 expect
函数之前,请确保导入 OneWayTesting 模块。
import OneWayTesting
您可以使用 expect
函数轻松检查状态值。
@Test
func incrementTwice() async {
await sut.send(.increment)
await sut.send(.increment)
await sut.expect(\.count, 2)
}
expect
函数在 XCTest
环境中以相同的方式使用。
func test_incrementTwice() async {
await sut.send(.increment)
await sut.send(.increment)
await sut.expect(\.count, 2)
}
有关更多详细信息,请参阅Testing 文章。
要了解如何更详细地使用 OneWay,请阅读文档。
OneWay | Swift | Xcode | 平台 |
---|---|---|---|
2.0 | 5.9 | 15.0 | iOS 13.0、macOS 10.15、tvOS 13.0、visionOS 1.0、watchOS 6.0 |
1.0 | 5.5 | 13.0 | iOS 13.0、macOS 10.15、tvOS 13.0、watchOS 6.0 |
OneWay 仅由 Swift Package Manager 支持。
要使用 Swift Package Manager 将 OneWay 集成到您的 Xcode 项目中,请将其添加到您的 Package.swift
的依赖项值中
dependencies: [
.package(url: "https://github.com/DevYeom/OneWay", from: "2.0.0"),
]
以下是提供诸多灵感的参考资料。
此库根据 MIT 许可证发布。 有关详细信息,请参阅 LICENSE。