{"id":1843,"url":"https://github.com/DevYeom/OneWay","last_synced_at":"2025-08-06T13:32:25.145Z","repository":{"id":38008804,"uuid":"494964641","full_name":"DevYeom/OneWay","owner":"DevYeom","description":"A Swift library for state management with unidirectional data flow.","archived":false,"fork":false,"pushed_at":"2024-10-22T12:17:19.000Z","size":2203,"stargazers_count":87,"open_issues_count":0,"forks_count":9,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-11-22T17:22:35.153Z","etag":null,"topics":["architecture","state-management","swift","unidirectional-data-flow"],"latest_commit_sha":null,"homepage":"https://swiftpackageindex.com/DevYeom/OneWay/2.10.0/documentation/oneway","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/DevYeom.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-05-22T05:09:23.000Z","updated_at":"2024-11-18T09:57:28.000Z","dependencies_parsed_at":"2024-03-31T13:29:55.377Z","dependency_job_id":"99c6cf2d-18e0-4723-bb15-8054758a09e7","html_url":"https://github.com/DevYeom/OneWay","commit_stats":{"total_commits":56,"total_committers":1,"mean_commits":56.0,"dds":0.0,"last_synced_commit":"d781ea95c9f873efbbcee3f8d6639b13a3136306"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DevYeom%2FOneWay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DevYeom%2FOneWay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DevYeom%2FOneWay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/DevYeom%2FOneWay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/DevYeom","download_url":"https://codeload.github.com/DevYeom/OneWay/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228905463,"owners_count":17989772,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["architecture","state-management","swift","unidirectional-data-flow"],"created_at":"2024-01-05T20:15:57.114Z","updated_at":"2024-12-09T14:30:51.694Z","avatar_url":"https://github.com/DevYeom.png","language":"Swift","funding_links":[],"categories":["Reactive Programming","Libs","other","Uncategorized"],"sub_categories":["Prototyping","Events","Uncategorized"],"readme":"\u003cimg src=\"https://github.com/DevYeom/OneWay/blob/assets/oneway_logo.png\" alt=\"oneway_logo\"/\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/DevYeom/OneWay/actions/workflows/ci.yml\"\u003e\n    \u003cimg alt=\"CI\" src=\"https://github.com/DevYeom/OneWay/actions/workflows/ci.yml/badge.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/DevYeom/OneWay/releases/latest\"\u003e\n    \u003cimg alt=\"release\" src=\"https://img.shields.io/github/v/release/DevYeom/OneWay?logo=swift\u0026color=orange\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"LICENSE\"\u003e\n    \u003cimg alt=\"license\" src=\"https://img.shields.io/badge/license-MIT-lightgray.svg\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://swiftpackageindex.com/DevYeom/OneWay\"\u003e\n    \u003cimg alt=\"Swift\" src=\"https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FDevYeom%2FOneWay%2Fbadge%3Ftype%3Dswift-versions\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://swiftpackageindex.com/DevYeom/OneWay\"\u003e\n    \u003cimg alt=\"Platforms\" src=\"https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FDevYeom%2FOneWay%2Fbadge%3Ftype%3Dplatforms\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n**OneWay** is a simple, lightweight library for state management using a unidirectional data flow, fully compatiable with Swift 6 and built on [Swift Concurrency](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/). Its structure makes it easier to maintain thread safety at all times.\n\nIt 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.\n\n- [Data Flow](#data-flow)\n- [Usage](#usage)\n- [Documentation](#documentation)\n- [Examples](#examples)\n- [Requirements](#requirements)\n- [Installation](#installation)\n- [References](#references)\n\n## Data Flow\n\nWhen using the `Store`, the data flow is as follows.\n\n\u003cimg src=\"https://github.com/DevYeom/OneWay/blob/assets/flow_description_v2_1.png\" alt=\"flow_description_1\"/\u003e\n\nWhen working on UI, it is better to use `ViewStore` to ensure main thread operation.\n\n\u003cimg src=\"https://github.com/DevYeom/OneWay/blob/assets/flow_description_v2_2.png\" alt=\"flow_description_1\"/\u003e\n\n## Usage\n\n### Implementing a Reducer\n\nAfter adopting the `Reducer` protocol, define the `Action` and `State`, and then implement the logic for each `Action` within the `reduce(state:action:)` function.\n\n```swift\nstruct CountingReducer: Reducer {\n    enum Action: Sendable {\n        case increment\n        case decrement\n        case twice\n        case setIsLoading(Bool)\n    }\n\n    struct State: Sendable, Equatable {\n        var number: Int\n        var isLoading: Bool\n    }\n\n    func reduce(state: inout State, action: Action) -\u003e AnyEffect\u003cAction\u003e {\n        switch action {\n        case .increment:\n            state.number += 1\n            return .none\n        case .decrement:\n            state.number -= 1\n            return .none\n        case .twice:\n            return .concat(\n                .just(.setIsLoading(true)),\n                .merge(\n                    .just(.increment),\n                    .just(.increment)\n                ),\n                .just(.setIsLoading(false))\n            )\n        case .setIsLoading(let isLoading):\n            state.isLoading = isLoading\n            return .none\n        }\n    }\n}\n```\n\n### Sending Actions\n\nSending an action to a **Store** causes changes in the `state` via `Reducer`.\n\n```swift\nlet store = Store(\n    reducer: CountingReducer(),\n    state: CountingReducer.State(number: 0)\n)\n\nawait store.send(.increment)\nawait store.send(.decrement)\nawait store.send(.twice)\n\nprint(await store.state.number) // 2\n```\n\nThe usage is the same for `ViewStore`. However, when working within `MainActor`, such as in `UIViewController` or `View`'s body, `await` can be omitted.\n\n```swift\nlet store = ViewStore(\n    reducer: CountingReducer(),\n    state: CountingReducer.State(number: 0)\n)\n\nstore.send(.increment)\nstore.send(.decrement)\nstore.send(.twice)\n\nprint(store.state.number) // 2\n```\n\n### Observing States\n\nWhen the state changes, you can receive a new state. It guarantees that the same state does not come down consecutively.\n\n```swift\nstruct State: Sendable, Equatable {\n    var number: Int\n}\n\n// number \u003c- 10, 10, 20 ,20\n\nfor await state in store.states {\n    print(state.number)\n}\n// Prints \"10\", \"20\"\n```\n\nOf course, you can observe specific properties only.\n\n```swift\n// number \u003c- 10, 10, 20 ,20\n\nfor await number in store.states.number {\n    print(number)\n}\n// Prints \"10\", \"20\"\n```\n\nIf 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](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/copyonwrite), [@Ignored](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/ignored)), refer to [here](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/triggered).\n\n```swift\nstruct State: Sendable, Equatable {\n    @Triggered var number: Int\n}\n\n// number \u003c- 10, 10, 20 ,20\n\nfor await state in store.states {\n    print(state.number)\n}\n// Prints \"10\", \"10\", \"20\", \"20\"\n```\n\nWhen 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](https://github.com/apple/swift-async-algorithms), you can remove duplicates as follows.\n\n```swift\nstruct State: Sendable, Equatable {\n    var number: Int\n    var text: String\n}\n\n// number \u003c- 10\n// text \u003c- \"a\", \"b\", \"c\"\n\nfor await number in store.states.number {\n    print(number)\n}\n// Prints \"10\", \"10\", \"10\"\n\nfor await number in store.states.number.removeDuplicates() {\n    print(number)\n}\n// Prints \"10\"\n```\n\n### Integration with SwiftUI\n\nIt can be seamlessly integrated with [SwiftUI](https://developer.apple.com/documentation/swiftui).\n\n```swift\nstruct CounterView: View {\n    @StateObject private var store = ViewStore(\n        reducer: CountingReducer(),\n        state: CountingReducer.State(number: 0)\n    )\n\n    var body: some View {\n        VStack {\n            Text(\"\\(store.state.number)\")\n            Toggle(\n                \"isLoading\",\n                isOn: Binding\u003cBool\u003e(\n                    get: { store.state.isLoading },\n                    set: { store.send(.setIsLoading($0)) }\n                )\n            )\n        }\n        .onAppear {\n            store.send(.increment)\n        }\n    }\n}\n```\n\nThere is also a helper function that makes it easy to create [Binding](https://developer.apple.com/documentation/swiftui/binding).\n\n```swift\nstruct CounterView: View {\n    @StateObject private var store = ViewStore(\n        reducer: CountingReducer(),\n        state: CountingReducer.State(number: 0)\n    )\n\n    var body: some View {\n        VStack {\n            Text(\"\\(store.state.number)\")\n            Toggle(\n                \"isLoading\",\n                isOn: store.binding(\\.isLoading, send: { .setIsLoading($0) })\n            )\n        }\n        .onAppear {\n            store.send(.increment)\n        }\n    }\n}\n```\n\nFor more details, please refer to the [examples](#examples).\n\n### Cancelling Effects\n\nYou can make an effect capable of being canceled by using `cancellable()`. And you can use `cancel()` to cancel a cancellable effect.\n\n```swift\nfunc reduce(state: inout State, action: Action) -\u003e AnyEffect\u003cAction\u003e {\n    switch action {\n// ...\n    case .request:\n        return .single {\n            let result = await api.result()\n            return Action.response(result)\n        }\n        .cancellable(\"requestID\")\n\n    case .cancel:\n        return .cancel(\"requestID\")\n// ...\n    }\n}\n```\n\nYou can assign anything that conforms [Hashable](https://developer.apple.com/documentation/swift/hashable) as an identifier for the effect, not just a string.\n\n```swift\nenum EffectID {\n    case request\n}\n\nfunc reduce(state: inout State, action: Action) -\u003e AnyEffect\u003cAction\u003e {\n    switch action {\n// ...\n    case .request:\n        return .single {\n            let result = await api.result()\n            return Action.response(result)\n        }\n        .cancellable(EffectID.request)\n\n    case .cancel:\n        return .cancel(EffectID.request)\n// ...\n    }\n}\n```\n\n### Various Effects\n\n**OneWay** supports various effects such as `just`, `concat`, `merge`, `single`, `sequence`, and more. For more details, please refer to the [documentation](https://swiftpackageindex.com/devyeom/oneway/main/documentation/oneway/effects).\n\n### External States\n\nYou 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`.\n\n```swift\nlet textPublisher = PassthroughSubject\u003cString, Never\u003e()\nlet numberPublisher = PassthroughSubject\u003cInt, Never\u003e()\n\nstruct CountingReducer: Reducer {\n// ...\n    func bind() -\u003e AnyEffect\u003cAction\u003e {\n        return .merge(\n            .sequence { send in\n                for await text in textPublisher.values {\n                    send(Action.response(text))\n                }\n            },\n            .sequence { send in\n                for await number in numberPublisher.values {\n                    send(Action.response(String(number)))\n                }\n            }\n        )\n    }\n// ...\n}\n```\n\n### Testing\n\n**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.\n\nBefore using the `expect` function, make sure to import the **OneWayTesting** module.\n\n```swift\nimport OneWayTesting\n```\n\n#### When using Testing\n\nYou can use the `expect` function to easily check the state value.\n\n```swift\n@Test\nfunc incrementTwice() async {\n    await sut.send(.increment)\n    await sut.send(.increment)\n\n    await sut.expect(\\.count, 2)\n}\n```\n\n#### When using XCTest\n\nThe `expect` function is used in the same way within the `XCTest` environment.\n\n```swift\nfunc test_incrementTwice() async {\n    await sut.send(.increment)\n    await sut.send(.increment)\n\n    await sut.expect(\\.count, 2)\n}\n```\n\nFor more details, please refer to the [Testing](https://swiftpackageindex.com/DevYeom/OneWay/main/documentation/OneWay/Testing) article.\n\n## Documentation\n\nTo learn how to use **OneWay** in more detail, go through the [documentation](https://swiftpackageindex.com/DevYeom/OneWay/main/documentation/OneWay).\n\n## Examples\n\n- [OneWayExample](https://github.com/DevYeom/OneWayExample)\n  - [UIKit](https://github.com/DevYeom/OneWayExample/tree/main/CounterUIKit/Counter)\n  - [SwiftUI](https://github.com/DevYeom/OneWayExample/tree/main/CounterSwiftUI/Counter)\n- [badabook-ios](https://github.com/OceanPositive/badabook-ios): A multi-platform application based on Clean Architecture.\n\n## Requirements\n\n| OneWay | Swift | Xcode | Platforms                                                   |\n|--------|-------|-------|-------------------------------------------------------------|\n| 2.0    | 5.9   | 15.0  | iOS 13.0, macOS 10.15, tvOS 13.0, visionOS 1.0, watchOS 6.0 |\n| 1.0    | 5.5   | 13.0  | iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0               |\n\n## Installation\n\n**OneWay** is only supported by Swift Package Manager.\n\nTo integrate **OneWay** into your Xcode project using Swift Package Manager, add it to the dependencies value of your `Package.swift`:\n\n```swift\ndependencies: [\n  .package(url: \"https://github.com/DevYeom/OneWay\", from: \"2.0.0\"),\n]\n```\n\n## References\n\nThese are the references that have provided much inspiration.\n\n- [Flux](https://github.com/facebook/flux)\n- [The Composable Architecture](https://github.com/pointfreeco/swift-composable-architecture)\n- [ReactorKit](https://github.com/ReactorKit/ReactorKit)\n- [awesome-state](https://github.com/tnfe/awesome-state)\n\n## License\n\nThis library is released under the MIT license. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDevYeom%2FOneWay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2FDevYeom%2FOneWay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2FDevYeom%2FOneWay/lists"}