ProductPromotion
Logo

Swift

made by https://0x3d.site

GitHub - DevYeom/OneWay: A Swift library for state management with unidirectional data flow.
A Swift library for state management with unidirectional data flow. - DevYeom/OneWay
Visit Site

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

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

OneWay is a simple, lightweight library for state management using a unidirectional data flow, fully compatiable with Swift 6 and built on Swift Concurrency. Its structure makes it easier to maintain thread safety at all times.

It integrates effortlessly across platforms and frameworks, with zero third-party dependencies, allowing you to use it in its purest form. OneWay can be used anywhere, not just in the presentation layer, to simplify the complex business logic. If you're looking to implement unidirectional logic, OneWay is a straightforward and practical solution.

Data Flow

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

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

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"

Integration with SwiftUI

It can be seamlessly integrated with 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)
        }
    }
}

There is also a helper function that makes it easy to create 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)
        }
    }
}

For more details, please refer to the examples.

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

Testing

OneWay provides the expect function to help you write concise and clear tests. This function works asynchronously, allowing you to verify whether the state updates as expected.

Before using the expect function, make sure to import the OneWayTesting module.

import OneWayTesting

When using Testing

You can use the expect function to easily check the state value.

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

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

When using XCTest

The expect function is used in the same way within the XCTest environment.

func test_incrementTwice() async {
    await sut.send(.increment)
    await sut.send(.increment)

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

For more details, please refer to the Testing article.

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.

More Resources
to explore the angular.

mail [email protected] to add your project or resources here 🔥.

Related Articles
to learn about angular.

FAQ's
to learn more about Angular JS.

mail [email protected] to add more queries here 🔍.

More Sites
to check out once you're finished browsing here.

0x3d
https://www.0x3d.site/
0x3d is designed for aggregating information.
NodeJS
https://nodejs.0x3d.site/
NodeJS Online Directory
Cross Platform
https://cross-platform.0x3d.site/
Cross Platform Online Directory
Open Source
https://open-source.0x3d.site/
Open Source Online Directory
Analytics
https://analytics.0x3d.site/
Analytics Online Directory
JavaScript
https://javascript.0x3d.site/
JavaScript Online Directory
GoLang
https://golang.0x3d.site/
GoLang Online Directory
Python
https://python.0x3d.site/
Python Online Directory
Swift
https://swift.0x3d.site/
Swift Online Directory
Rust
https://rust.0x3d.site/
Rust Online Directory
Scala
https://scala.0x3d.site/
Scala Online Directory
Ruby
https://ruby.0x3d.site/
Ruby Online Directory
Clojure
https://clojure.0x3d.site/
Clojure Online Directory
Elixir
https://elixir.0x3d.site/
Elixir Online Directory
Elm
https://elm.0x3d.site/
Elm Online Directory
Lua
https://lua.0x3d.site/
Lua Online Directory
C Programming
https://c-programming.0x3d.site/
C Programming Online Directory
C++ Programming
https://cpp-programming.0x3d.site/
C++ Programming Online Directory
R Programming
https://r-programming.0x3d.site/
R Programming Online Directory
Perl
https://perl.0x3d.site/
Perl Online Directory
Java
https://java.0x3d.site/
Java Online Directory
Kotlin
https://kotlin.0x3d.site/
Kotlin Online Directory
PHP
https://php.0x3d.site/
PHP Online Directory
React JS
https://react.0x3d.site/
React JS Online Directory
Angular
https://angular.0x3d.site/
Angular JS Online Directory