oneway_logo

CI release license

Swift Platforms

OneWay 是一个简单轻量级的状态管理库,使用单向数据流,完全兼容 Swift 6 并且构建于 Swift 并发之上。它的结构使其更容易始终保持线程安全。

它可以轻松地跨平台和框架集成,没有任何第三方依赖,允许您以其最纯粹的形式使用它。 OneWay 可以用于任何地方,不仅仅是展示层,来简化复杂的业务逻辑。如果您正在寻找实现单向逻辑的方法,OneWay 是一个直接且实用的解决方案。

数据流

当使用 Store 时,数据流如下所示。

flow_description_1

在处理 UI 时,最好使用 ViewStore 以确保主线程操作。

flow_description_1

用法

实现 Reducer

在采纳 Reducer 协议之后,定义 ActionState,然后在 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
        }
    }
}

发送 Actions

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 中工作时,例如在 UIViewControllerView 的 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 集成

它可以与 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)
        }
    }
}

有关更多详细信息,请参阅示例

取消 Effects

您可以通过使用 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)
// ...
    }
}

各种 Effects

OneWay 支持各种 effects,例如 justconcatmergesinglesequence 等。 有关更多详细信息,请参阅文档

外部状态

您可以通过实现 bind() 轻松接收外部状态。 如果 publishers 或 streams 中有需要重新绑定的更改,您可以调用 Storereset()

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

当使用 Testing

您可以使用 expect 函数轻松检查状态值。

@Test
func incrementTwice() async {
    await sut.send(.increment)
    await sut.send(.increment)

    await sut.expect(\.count, 2)
}

当使用 XCTest

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