├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── README.md ├── Sources └── ComposableArchitectureExtras │ └── TaskResult+VoidSuccess.swift └── Tests └── ComposableArchitectureExtrasTests └── ComposableArchitectureExtrasTests.swift /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | branches: [ "main" ] 6 | paths-ignore: 7 | - README.md 8 | push: 9 | branches: [ "main" ] 10 | paths-ignore: 11 | - README.md 12 | workflow_dispatch: 13 | 14 | concurrency: 15 | group: format-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | name: Test 21 | strategy: 22 | fail-fast: false 23 | matrix: 24 | os: [macos-13] 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v4 29 | 30 | - name: Select Xcode 15 31 | run: sudo xcode-select -s /Applications/Xcode_15.0.app 32 | 33 | - name: Test 34 | run: swift test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.swiftinterface 6 | /*.xcodeproj 7 | xcuserdata/ 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Ryu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "combine-schedulers", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/pointfreeco/combine-schedulers", 7 | "state" : { 8 | "revision" : "9dc9cbe4bc45c65164fa653a563d8d8db61b09bb", 9 | "version" : "1.0.0" 10 | } 11 | }, 12 | { 13 | "identity" : "swift-case-paths", 14 | "kind" : "remoteSourceControl", 15 | "location" : "https://github.com/pointfreeco/swift-case-paths", 16 | "state" : { 17 | "revision" : "5da6989aae464f324eef5c5b52bdb7974725ab81", 18 | "version" : "1.0.0" 19 | } 20 | }, 21 | { 22 | "identity" : "swift-clocks", 23 | "kind" : "remoteSourceControl", 24 | "location" : "https://github.com/pointfreeco/swift-clocks", 25 | "state" : { 26 | "revision" : "d1fd837326aa719bee979bdde1f53cd5797443eb", 27 | "version" : "1.0.0" 28 | } 29 | }, 30 | { 31 | "identity" : "swift-collections", 32 | "kind" : "remoteSourceControl", 33 | "location" : "https://github.com/apple/swift-collections", 34 | "state" : { 35 | "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", 36 | "version" : "1.0.4" 37 | } 38 | }, 39 | { 40 | "identity" : "swift-composable-architecture", 41 | "kind" : "remoteSourceControl", 42 | "location" : "https://github.com/pointfreeco/swift-composable-architecture.git", 43 | "state" : { 44 | "revision" : "9b0f600253f467f61cbd53f60ccc243cc4ff27cd", 45 | "version" : "1.3.0" 46 | } 47 | }, 48 | { 49 | "identity" : "swift-concurrency-extras", 50 | "kind" : "remoteSourceControl", 51 | "location" : "https://github.com/pointfreeco/swift-concurrency-extras", 52 | "state" : { 53 | "revision" : "ea631ce892687f5432a833312292b80db238186a", 54 | "version" : "1.0.0" 55 | } 56 | }, 57 | { 58 | "identity" : "swift-custom-dump", 59 | "kind" : "remoteSourceControl", 60 | "location" : "https://github.com/pointfreeco/swift-custom-dump", 61 | "state" : { 62 | "revision" : "edd66cace818e1b1c6f1b3349bb1d8e00d6f8b01", 63 | "version" : "1.0.0" 64 | } 65 | }, 66 | { 67 | "identity" : "swift-dependencies", 68 | "kind" : "remoteSourceControl", 69 | "location" : "https://github.com/pointfreeco/swift-dependencies", 70 | "state" : { 71 | "revision" : "4e1eb6e28afe723286d8cc60611237ffbddba7c5", 72 | "version" : "1.0.0" 73 | } 74 | }, 75 | { 76 | "identity" : "swift-identified-collections", 77 | "kind" : "remoteSourceControl", 78 | "location" : "https://github.com/pointfreeco/swift-identified-collections", 79 | "state" : { 80 | "revision" : "d1e45f3e1eee2c9193f5369fa9d70a6ddad635e8", 81 | "version" : "1.0.0" 82 | } 83 | }, 84 | { 85 | "identity" : "swiftui-navigation", 86 | "kind" : "remoteSourceControl", 87 | "location" : "https://github.com/pointfreeco/swiftui-navigation", 88 | "state" : { 89 | "revision" : "f5bcdac5b6bb3f826916b14705f37a3937c2fd34", 90 | "version" : "1.0.0" 91 | } 92 | }, 93 | { 94 | "identity" : "xctest-dynamic-overlay", 95 | "kind" : "remoteSourceControl", 96 | "location" : "https://github.com/pointfreeco/xctest-dynamic-overlay", 97 | "state" : { 98 | "revision" : "23cbf2294e350076ea4dbd7d5d047c1e76b03631", 99 | "version" : "1.0.2" 100 | } 101 | } 102 | ], 103 | "version" : 2 104 | } 105 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.7 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "swift-composable-architecture-extras", 8 | platforms: [ 9 | .iOS(.v13), 10 | .macOS(.v10_15), 11 | .tvOS(.v13), 12 | .watchOS(.v6), 13 | ], 14 | products: [ 15 | // Products define the executables and libraries a package produces, making them visible to other packages. 16 | .library( 17 | name: "ComposableArchitectureExtras", 18 | targets: ["ComposableArchitectureExtras"] 19 | ), 20 | ], 21 | dependencies: [ 22 | .package(url: "https://github.com/pointfreeco/swift-composable-architecture.git", "1.0.0"..<"2.0.0") 23 | ], 24 | targets: [ 25 | // Targets are the basic building blocks of a package, defining a module or a test suite. 26 | // Targets can depend on other targets in this package and products from dependencies. 27 | .target( 28 | name: "ComposableArchitectureExtras", 29 | dependencies: [ 30 | .product(name: "ComposableArchitecture", package: "swift-composable-architecture") 31 | ] 32 | ), 33 | .testTarget( 34 | name: "ComposableArchitectureExtrasTests", 35 | dependencies: ["ComposableArchitectureExtras"] 36 | ), 37 | ] 38 | ) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Composable Architecture Extras 2 | ![Language:Swift](https://img.shields.io/static/v1?label=Language&message=Swift&color=orange&style=flat-square) 3 | ![License:MIT](https://img.shields.io/static/v1?label=License&message=MIT&color=blue&style=flat-square) 4 | [![Latest Release](https://img.shields.io/github/v/release/Ryu0118/swift-composable-architecture-extras?style=flat-square)](https://github.com/Ryu0118/swift-composable-architecture-extras/releases/latest) 5 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRyu0118%2Fswift-composable-architecture-extras%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/Ryu0118/swift-composable-architecture-extras) 6 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2FRyu0118%2Fswift-composable-architecture-extras%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/Ryu0118/swift-composable-architecture-extras) 7 | [![Twitter](https://img.shields.io/twitter/follow/ryu_hu03?style=social)](https://twitter.com/ryu_hu03) 8 | 9 | 10 | A companion library to Point-Free's [swift-composable-architecture](https://github.com/pointfreeco/swift-composable-architecture). 11 | 12 | * [TaskResult+VoidSuccess](#taskresult-and-voidsuccess) 13 | * [Installation](#installation) 14 | 15 | ## TaskResult And VoidSuccess 16 | In the current TCA, TaskResult was not Equatable compliant, so it must be written like this 17 | ```Swift 18 | public struct HogeFeature: Reducer { 19 | public struct State: Equatable {} 20 | public enum Action: Equatable { 21 | case onAppear 22 | case response(TaskResult) 23 | } 24 | 25 | public func reduce(into state: inout State, action: Action) -> Effect { 26 | switch action { 27 | case .onAppear: 28 | return .run { send in 29 | await send( 30 | .response( 31 | TaskResult { 32 | try await asyncThrowsVoidFunc() 33 | return VoidSuccess() 34 | } 35 | ) 36 | ) 37 | } 38 | case .response(.success): 39 | // do something 40 | return .none 41 | 42 | case .response(.failure(let error)): 43 | // handle error 44 | return .none 45 | } 46 | } 47 | } 48 | ``` 49 | it is not kind to create an `VoidSuccess` on user just to conform `TaskResult` to `Equatable` even though `Void` is already provided in Swift. 50 | So we extended TaskResult so that it could be written like this. 51 | ```Swift 52 | public struct HogeFeature: Reducer { 53 | public struct State: Equatable {} 54 | public enum Action: Equatable { 55 | case onAppear 56 | case response(TaskResult) 57 | } 58 | 59 | public func reduce(into state: inout State, action: Action) -> Effect { 60 | switch action { 61 | case .onAppear: 62 | return .run { send in 63 | await send( 64 | .response( 65 | TaskResult { 66 | try await asyncThrowsVoidFunc() 67 | } 68 | ) 69 | ) 70 | } 71 | case .response(.success): 72 | // do something 73 | return .none 74 | 75 | case .response(.failure(let error)): 76 | // handle error 77 | return .none 78 | } 79 | } 80 | } 81 | ``` 82 | 83 | ## Installation 84 | In the dependencies section, add: 85 | ```Swift 86 | .package(url: "https://github.com/Ryu0118/swift-composable-architecture-extras", from: "1.0.0") 87 | ``` 88 | In each module, add: 89 | ```Swift 90 | .target( 91 | name: "MyModule", 92 | dependencies: [ 93 | .product(name: "ComposableArchitectureExtras", package: "swift-composable-architecture-extras") 94 | ] 95 | ), 96 | ``` 97 | -------------------------------------------------------------------------------- /Sources/ComposableArchitectureExtras/TaskResult+VoidSuccess.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | 3 | /// A marker type indicating a successful void result. 4 | public struct VoidSuccess: Codable, Sendable, Hashable { 5 | public init() {} 6 | } 7 | 8 | /// An extension on `TaskResult` where the success type is `VoidSuccess`. 9 | extension TaskResult where Success == VoidSuccess { 10 | /// Creates a new task result by evaluating an async throwing closure. 11 | /// 12 | /// This initializer is used to handle asynchronous operations that produce a result of type `Void`. 13 | /// The provided async, throwing closure is executed in an asynchronous context. If the closure 14 | /// succeeds, the `TaskResult` is created with a success marker of type `VoidSuccess`. If the 15 | /// closure throws an error, the `TaskResult` is generated with the provided error as a failure. 16 | /// 17 | /// This initializer is often used within an async effect that's returned from a reducer. 18 | /// 19 | /// - Parameter body: An async, throwing closure. 20 | public init(catching body: @Sendable () async throws -> Void) async { 21 | do { 22 | try await body() 23 | self = .success(VoidSuccess()) 24 | } catch { 25 | self = .failure(error) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/ComposableArchitectureExtrasTests/ComposableArchitectureExtrasTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ComposableArchitectureExtras 3 | 4 | final class swift_composable_architecture_extrasTests: XCTestCase { 5 | func testExample() throws { 6 | // XCTest Documenation 7 | // https://developer.apple.com/documentation/xctest 8 | 9 | // Defining Test Cases and Test Methods 10 | // https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods 11 | } 12 | } 13 | --------------------------------------------------------------------------------