OneWay

2.6.0

A Swift library for state management with unidirectional data flow.
DevYeom/OneWay

What's New

2.6.0

2024-06-01T04:02:27Z

What's Changed

  • Change property wrappers to intuitive names by @DevYeom in #82

Full Changelog: 2.5.0...2.6.0

oneway_logo

release CI license

Swift Platforms

OneWay is a remarkably simple and lightweight library designed for state management through unidirectional data flow. It is implemented based on Swift Concurrency. The Store is implemented with an Actor, making it always thread-safe.

Whether you're working on any platform or within any framework, OneWay can seamlessly integrate. With zero third-party dependencies, OneWay can be used in its purest form. This library isn't limited to use only for the presentation layer. It's also useful for streamlining intricate business logic. You'll find it beneficial whenever you seek to implement logic in a unidirectional manner.

Data Flow

When using the Store, the data flow is as follows.

flow_description_1

When working on UI, it is better to use ViewStore to ensure main thread operation.

flow_description_1

Usage

Implementing a Reducer

After adopting the Reducer protocol, define the Action and State, and then implement the logic for each Action within the reduce(state:action:) function.

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
        }
    }
}

Sending Actions

Sending an action to a Store causes changes in the state via Reducer.

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

The usage is the same for ViewStore. However, when working within MainActor, such as in UIViewController or View's body, await can be omitted.

let store = ViewStore(
    reducer: CountingReducer(),
    state: CountingReducer.State(number: 0)
)

store.send(.increment)
store.send(.decrement)
store.send(.twice)

print(store.state.number) // 2

Observing States

When the state changes, you can receive a new state. It guarantees that the same state does not come down consecutively.

struct State: Sendable, Equatable {
    var number: Int
}

// number <- 10, 10, 20 ,20

for await state in store.states {
    print(state.number)
}
// Prints "10", "20"

Of course, you can observe specific properties only.

// number <- 10, 10, 20 ,20

for await number in store.states.number {
    print(number)
}
// Prints "10", "20"

If you want to continue receiving the value even when the same value is assigned to the State, you can use @Triggered. For explanations of other useful property wrappers(e.g. @CopyOnWrite, @Ignored), refer to here.

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"

When there are multiple properties of the state, it is possible for the state to change due to other properties that are not subscribed to. In such cases, if you are using AsyncAlgorithms, you can remove duplicates as follows.

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"

Cancelling Effects

You can make an effect capable of being canceled by using cancellable(). And you can use cancel() to cancel a cancellable 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")
// ...
    }
}

You can assign anything that conforms Hashable as an identifier for the effect, not just a string.

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)
// ...
    }
}

Various Effects

OneWay supports various effects such as just, concat, merge, single, sequence, and more. For more details, please refer to the documentation.

External States

You can easily receive to external states by implementing bind(). If there are changes in publishers or streams that necessitate rebinding, you can call reset() of Store.

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)))
                }
            }
        )
    }
// ...
}

Documentation

To learn how to use OneWay in more detail, go through the documentation.

Examples

Requirements

OneWay Swift Xcode Platforms
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

Installation

OneWay is only supported by Swift Package Manager.

To integrate OneWay into your Xcode project using Swift Package Manager, add it to the dependencies value of your Package.swift:

dependencies: [
  .package(url: "https://github.com/DevYeom/OneWay", from: "2.0.0"),
]

References

These are the references that have provided much inspiration.

License

This library is released under the MIT license. See LICENSE for details.

Description

  • Swift Tools 5.9.0
View More Packages from this Author

Dependencies

Last updated: Tue Nov 19 2024 12:29:08 GMT-1000 (Hawaii-Aleutian Standard Time)