├── .gitignore ├── Sources ├── _CAsyncSupport │ └── module.modulemap ├── ComposableArchitecture │ ├── Documentation.docc │ │ └── Extensions │ │ │ ├── AnyReducerDeprecations.md │ │ │ ├── SwitchStore.md │ │ │ ├── EffectCancel.md │ │ │ ├── EffectCancelIds.md │ │ │ ├── TestStoreExhaustivity.md │ │ │ ├── EffectSend.md │ │ │ ├── EffectRun.md │ │ │ ├── StoreScope.md │ │ │ ├── Deprecations │ │ │ ├── ReduceDeprecations.md │ │ │ ├── ReducerDeprecations.md │ │ │ ├── StoreDeprecations.md │ │ │ ├── SwiftUIDeprecations.md │ │ │ ├── ViewStoreDeprecations.md │ │ │ ├── TestStoreDeprecations.md │ │ │ └── EffectDeprecations.md │ │ │ ├── EffectCancellable.md │ │ │ ├── WithTaskCancellation.md │ │ │ ├── ViewStoreBinding.md │ │ │ ├── WithViewStore.md │ │ │ ├── Reduce.md │ │ │ ├── WithViewStoreInit.md │ │ │ ├── TaskResult.md │ │ │ ├── Store.md │ │ │ ├── UIKit.md │ │ │ ├── ReducerBuilder.md │ │ │ ├── ViewStore.md │ │ │ ├── SwiftUI.md │ │ │ ├── ReducerProtocol.md │ │ │ ├── Effect.md │ │ │ └── TestStore.md │ ├── Internal │ │ ├── Exports.swift │ │ ├── Box.swift │ │ ├── Debug.swift │ │ ├── Binding+IsPresent.swift │ │ ├── TypeName.swift │ │ ├── Locking.swift │ │ ├── TaskCancellableValue.swift │ │ ├── OpenExistential.swift │ │ ├── CurrentValueRelay.swift │ │ └── RuntimeWarnings.swift │ ├── Reducer │ │ └── Reducers │ │ │ ├── EmptyReducer.swift │ │ │ ├── Optional.swift │ │ │ ├── BindingReducer.swift │ │ │ ├── Reduce.swift │ │ │ └── CombineReducers.swift │ ├── Effects │ │ └── Publisher │ │ │ └── Deferring.swift │ └── SwiftUI │ │ └── Alert.swift └── swift-composable-architecture-benchmark │ ├── main.swift │ ├── ViewStore.swift │ ├── StoreScope.swift │ ├── Effects.swift │ ├── Dependencies.swift │ └── Common.swift ├── Examples ├── Todos │ ├── Todos │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ ├── TodosApp.swift │ │ └── Todo.swift │ ├── Todos.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── README.md ├── Search │ ├── Search │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ └── SearchApp.swift │ ├── Search.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── README.md ├── TicTacToe │ ├── App │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ ├── TicTacToeApp.swift │ │ └── RootView.swift │ ├── tic-tac-toe │ │ ├── .gitignore │ │ ├── Sources │ │ │ ├── AppSwiftUI │ │ │ │ └── AppView.swift │ │ │ ├── AuthenticationClientLive │ │ │ │ └── LiveAuthenticationClient.swift │ │ │ ├── GameCore │ │ │ │ └── Three.swift │ │ │ ├── AppCore │ │ │ │ └── AppCore.swift │ │ │ ├── NewGameCore │ │ │ │ └── NewGameCore.swift │ │ │ ├── AppUIKit │ │ │ │ └── AppViewController.swift │ │ │ ├── TwoFactorCore │ │ │ │ └── TwoFactorCore.swift │ │ │ ├── AuthenticationClient │ │ │ │ └── AuthenticationClient.swift │ │ │ └── LoginCore │ │ │ │ └── LoginCore.swift │ │ └── Tests │ │ │ ├── NewGameSwiftUITests │ │ │ └── NewGameSwiftUITests.swift │ │ │ ├── NewGameCoreTests │ │ │ └── NewGameCoreTests.swift │ │ │ ├── TwoFactorCoreTests │ │ │ └── TwoFactorCoreTests.swift │ │ │ ├── GameCoreTests │ │ │ └── GameCoreTests.swift │ │ │ └── TwoFactorSwiftUITests │ │ │ └── TwoFactorSwiftUITests.swift │ ├── TicTacToe.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── README.md ├── VoiceMemos │ ├── VoiceMemos │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ ├── Helpers.swift │ │ ├── VoiceMemosApp.swift │ │ ├── AudioPlayerClient │ │ │ ├── AudioPlayerClient.swift │ │ │ └── LiveAudioPlayerClient.swift │ │ ├── Dependencies.swift │ │ ├── AudioRecorderClient │ │ │ └── AudioRecorderClient.swift │ │ └── Info.plist │ ├── VoiceMemos.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── README.md ├── CaseStudies │ ├── tvOSCaseStudies │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ └── Contents.json │ │ ├── Core.swift │ │ ├── AppDelegate.swift │ │ ├── RootView.swift │ │ └── Info.plist │ ├── SwiftUICaseStudies │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ ├── Internal │ │ │ ├── AboutView.swift │ │ │ ├── UIViewRepresented.swift │ │ │ ├── CircularProgressView.swift │ │ │ ├── ResignFirstResponder.swift │ │ │ └── TemplateText.swift │ │ ├── CaseStudiesApp.swift │ │ ├── FactClient.swift │ │ ├── 04-HigherOrderReducers-ResuableOfflineDownloads │ │ │ └── DownloadClient.swift │ │ ├── Info.plist │ │ ├── 01-GettingStarted-Composition-TwoCounters.swift │ │ └── 01-GettingStarted-Counter.swift │ ├── UIKitCaseStudies │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── Internal │ │ │ ├── UIViewRepresented.swift │ │ │ ├── ActivityIndicatorViewController.swift │ │ │ └── IfLetStoreController.swift │ │ ├── SceneDelegate.swift │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── README.md │ ├── CaseStudies.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── UIKitCaseStudiesTests │ │ ├── Info.plist │ │ └── UIKitCaseStudiesTests.swift │ ├── SwiftUICaseStudiesTests │ │ ├── 01-GettingStarted-BindingBasicsTests.swift │ │ ├── 02-Effects-LongLivingTests.swift │ │ ├── 02-Effects-TimersTests.swift │ │ ├── 04-HigherOrderReducers-LifecycleTests.swift │ │ ├── 04-HigherOrderReducers-RecursionTests.swift │ │ ├── 02-Effects-RefreshableTests.swift │ │ ├── 02-Effects-BasicsTests.swift │ │ └── 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift │ └── tvOSCaseStudiesTests │ │ └── FocusTests.swift ├── Integration │ ├── Integration │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── EscapedWithViewStoreTestCase.swift │ │ ├── NavigationStackBindingTestCase.swift │ │ ├── IntegrationApp.swift │ │ └── ForEachBindingTestCase.swift │ ├── Integration.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── IntegrationUITests │ │ ├── NavigationStackBindingTests.swift │ │ ├── ForEachBindingTests.swift │ │ └── EscapedWithViewStoreTests.swift ├── SpeechRecognition │ ├── SpeechRecognition │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ ├── transparent.png │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ └── Contents.json │ │ ├── SpeechRecognitionApp.swift │ │ └── Info.plist │ └── README.md ├── Package.swift ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata └── README.md ├── ComposableArchitecture.xcworkspace ├── xcshareddata │ ├── IDEWorkspaceChecks.plist │ └── WorkspaceSettings.xcsettings └── contents.xcworkspacedata ├── .spi.yml ├── Tests └── ComposableArchitectureTests │ ├── SerialExecutor.swift │ ├── StoreFilterTests.swift │ ├── DeprecatedTests.swift │ ├── BindingLocalTests.swift │ ├── TaskCancellationTests.swift │ ├── IfLetReducerTests.swift │ ├── BindingTests.swift │ ├── EffectFailureTests.swift │ ├── MemoryManagementTests.swift │ ├── DebugTests.swift │ └── EffectDeferredTests.swift ├── .github ├── ISSUE_TEMPLATE │ └── config.yml └── workflows │ ├── format.yml │ ├── ci.yml │ └── documentation.yml ├── LICENSE └── Makefile /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /Sources/_CAsyncSupport/module.modulemap: -------------------------------------------------------------------------------- 1 | module _CAsyncSupport [system] { 2 | header "_CAsyncSupport.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Examples/Todos/Todos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Search/Search/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/TicTacToe/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudies/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Integration/Integration/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Integration/Integration/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Examples/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.2 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "Examples", 7 | products: [], 8 | targets: [] 9 | ) 10 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | xcuserdata/ 6 | DerivedData/ 7 | .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata 8 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/AnyReducerDeprecations.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/AnyReducer`` 2 | 3 | ## Topics 4 | 5 | ### Types 6 | 7 | - ``ActionFormat`` 8 | -------------------------------------------------------------------------------- /Examples/CaseStudies/README.md: -------------------------------------------------------------------------------- 1 | # Composable Architecture Case Studies 2 | 3 | This project includes a number of digestible examples of how to solve common problems using the Composable Architecture. 4 | -------------------------------------------------------------------------------- /Examples/TicTacToe/App/TicTacToeApp.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | @main 4 | struct TicTacToeApp: App { 5 | var body: some Scene { 6 | WindowGroup { 7 | RootView() 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/SwitchStore.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/SwitchStore`` 2 | 3 | ## Topics 4 | 5 | ### Building Content 6 | 7 | - ``CaseLet`` 8 | - ``Default`` 9 | -------------------------------------------------------------------------------- /Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancel.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/EffectPublisher/cancel(id:)-6hzsl`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``cancel(id:)-1c1dw`` 8 | -------------------------------------------------------------------------------- /Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancelIds.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/EffectPublisher/cancel(ids:)-8gan2`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``cancel(ids:)-1cqqx`` 8 | -------------------------------------------------------------------------------- /Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStoreExhaustivity.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/TestStore/exhaustivity`` 2 | 3 | ## Topics 4 | 5 | ### Configuring exhaustivity 6 | 7 | - ``Exhaustivity`` 8 | -------------------------------------------------------------------------------- /Examples/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectSend.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/EffectPublisher/send(_:)`` 2 | 3 | ## Topics 4 | 5 | ### Animating actions 6 | 7 | - ``EffectPublisher/send(_:animation:)`` 8 | -------------------------------------------------------------------------------- /Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectRun.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/EffectPublisher/run(priority:operation:catch:file:fileID:line:)`` 2 | 3 | ## Topics 4 | 5 | ### Sending actions 6 | 7 | - ``Send`` 8 | -------------------------------------------------------------------------------- /Examples/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/tvOSCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Examples/Integration/Integration.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/StoreScope.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/Store/scope(state:action:)`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``scope(state:)`` 8 | - ``stateless`` 9 | - ``actionless`` 10 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/Integration/Integration/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReduceDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | Review unsupported `Reduce` APIs. 4 | 5 | ## Topics 6 | 7 | ### Reducer structure 8 | 9 | - ``Reduce/init(_:environment:)`` 10 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/EffectCancellable.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/EffectPublisher/cancellable(id:cancelInFlight:)-499iv`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``cancellable(id:cancelInFlight:)-17skv`` 8 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Sources/swift-composable-architecture-benchmark/main.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import ComposableArchitecture 3 | 4 | Benchmark.main([ 5 | defaultBenchmarkSuite, 6 | dependenciesSuite, 7 | effectSuite, 8 | storeScopeSuite, 9 | viewStoreSuite, 10 | ]) 11 | -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thebrowsercompany/swift-composable-architecture/HEAD/Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/WithTaskCancellation.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/withTaskCancellation(id:cancelInFlight:operation:)-4dtr6`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``withTaskCancellation(id:cancelInFlight:operation:)-88kxz`` 8 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Internal/AboutView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct AboutView: View { 4 | let readMe: String 5 | 6 | var body: some View { 7 | DisclosureGroup("About this case study") { 8 | Text(template: self.readMe) 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/ViewStoreBinding.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/ViewStore/binding(get:send:)-65xes`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``binding(get:send:)-l66r`` 8 | - ``binding(send:)-7nwak`` 9 | - ``binding(send:)-705m7`` 10 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Helpers.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | let dateComponentsFormatter: DateComponentsFormatter = { 4 | let formatter = DateComponentsFormatter() 5 | formatter.allowedUnits = [.minute, .second] 6 | formatter.zeroFormattingBehavior = .pad 7 | return formatter 8 | }() 9 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/Exports.swift: -------------------------------------------------------------------------------- 1 | @_exported import CasePaths 2 | @_exported import Clocks 3 | @_exported import CombineSchedulers 4 | @_exported import CustomDump 5 | @_exported import Dependencies 6 | @_exported import IdentifiedCollections 7 | @_exported import _SwiftUINavigationState 8 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStore.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/WithViewStore`` 2 | 3 | ## Overview 4 | 5 | ## Topics 6 | 7 | ### Creating a view 8 | 9 | - ``init(_:observe:content:file:line:)`` 10 | 11 | ### Debugging view updates 12 | 13 | - ``debug(_:)`` 14 | -------------------------------------------------------------------------------- /Examples/Integration/Integration/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Reduce.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/Reduce`` 2 | 3 | ## Topics 4 | 5 | ### Creating a reducer 6 | 7 | - ``init(_:)-17fld`` 8 | 9 | ### Type erased reducers 10 | 11 | - ``init(_:)-3rph8`` 12 | 13 | ### Deprecations 14 | 15 | - 16 | -------------------------------------------------------------------------------- /ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/Box.swift: -------------------------------------------------------------------------------- 1 | final class Box { 2 | var wrappedValue: Wrapped 3 | 4 | init(wrappedValue: Wrapped) { 5 | self.wrappedValue = wrappedValue 6 | } 7 | 8 | var boxedValue: Wrapped { 9 | _read { yield self.wrappedValue } 10 | _modify { yield &self.wrappedValue } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ComposableArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/Integration/Integration.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/Debug.swift: -------------------------------------------------------------------------------- 1 | import CustomDump 2 | import Foundation 3 | 4 | extension String { 5 | @usableFromInline 6 | func indent(by indent: Int) -> String { 7 | let indentation = String(repeating: " ", count: indent) 8 | return indentation + self.replacingOccurrences(of: "\n", with: "\n\(indentation)") 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.spi.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | builder: 3 | configs: 4 | - platform: ios 5 | scheme: ComposableArchitecture 6 | - platform: macos-xcodebuild 7 | scheme: ComposableArchitecture 8 | - platform: tvos 9 | scheme: ComposableArchitecture 10 | - platform: watchos 11 | scheme: ComposableArchitecture 12 | - documentation_targets: [ComposableArchitecture] 13 | -------------------------------------------------------------------------------- /Examples/Todos/Todos/TodosApp.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | @main 5 | struct TodosApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | AppView( 9 | store: Store( 10 | initialState: Todos.State(), 11 | reducer: Todos()._printChanges() 12 | ) 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Search/Search/SearchApp.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | @main 5 | struct SearchApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | SearchView( 9 | store: Store( 10 | initialState: Search.State(), 11 | reducer: Search()._printChanges() 12 | ) 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/Todos/README.md: -------------------------------------------------------------------------------- 1 | # Todos 2 | 3 | This simple todo application built with the Composable Architecture includes a few bells and whistles: 4 | 5 | * Filtering and rearranging todo items. 6 | * Automatically sort completed todos to the bottom of the list. 7 | * Debouncing the sort action to allow multiple todo items to be toggled before being sorted. 8 | * A comprehensive test suite. 9 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/VoiceMemosApp.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | @main 5 | struct VoiceMemosApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | VoiceMemosView( 9 | store: Store( 10 | initialState: VoiceMemos.State(), 11 | reducer: VoiceMemos()._printChanges() 12 | ) 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudies/Core.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | 3 | struct Root: ReducerProtocol { 4 | struct State { 5 | var focus = Focus.State() 6 | } 7 | 8 | enum Action { 9 | case focus(Focus.Action) 10 | } 11 | 12 | var body: some ReducerProtocol { 13 | Scope(state: \.focus, action: /Action.focus) { 14 | Focus() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/WithViewStoreInit.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/WithViewStore/init(_:observe:content:file:line:)`` 2 | 3 | ## Topics 4 | 5 | ### Overloads 6 | 7 | - ``WithViewStore/init(_:observe:removeDuplicates:content:file:line:)`` 8 | - ``WithViewStore/init(_:observe:send:content:file:line:)`` 9 | - ``WithViewStore/init(_:observe:send:removeDuplicates:content:file:line:)`` 10 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/Binding+IsPresent.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Binding { 4 | func isPresent() -> Binding where Value == Wrapped? { 5 | .init( 6 | get: { self.wrappedValue != nil }, 7 | set: { isPresent, transaction in 8 | guard !isPresent else { return } 9 | self.transaction(transaction).wrappedValue = nil 10 | } 11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/CaseStudiesApp.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | @main 5 | struct CaseStudiesApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | RootView( 9 | store: Store( 10 | initialState: Root.State(), 11 | reducer: Root() 12 | .signpost() 13 | ._printChanges() 14 | ) 15 | ) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ReducerDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | Review unsupported reducer APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. Select an API to see the replacement that you should use 8 | instead. 9 | 10 | ## Topics 11 | 12 | ### Reducer structure 13 | 14 | - ``AnyReducer`` 15 | - ``Reducer`` 16 | - ``DebugEnvironment`` 17 | -------------------------------------------------------------------------------- /Examples/SpeechRecognition/SpeechRecognition/SpeechRecognitionApp.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | @main 5 | struct SpeechRecognitionApp: App { 6 | var body: some Scene { 7 | WindowGroup { 8 | SpeechRecognitionView( 9 | store: Store( 10 | initialState: SpeechRecognition.State(), 11 | reducer: SpeechRecognition()._printChanges() 12 | ) 13 | ) 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/TaskResult.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/TaskResult`` 2 | 3 | ## Topics 4 | 5 | ### Representing a task result 6 | 7 | - ``success(_:)`` 8 | - ``failure(_:)`` 9 | 10 | ### Converting a throwing expression 11 | 12 | - ``init(catching:)`` 13 | 14 | ### Accessing a result's value 15 | 16 | - ``value`` 17 | 18 | ### Transforming results 19 | 20 | - ``map(_:)`` 21 | - ``flatMap(_:)`` 22 | - ``init(_:)`` 23 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Store.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/Store`` 2 | 3 | ## Topics 4 | 5 | ### Creating a store 6 | 7 | - ``init(initialState:reducer:prepareDependencies:)`` 8 | - ``StoreOf`` 9 | 10 | ### Scoping stores 11 | 12 | - ``scope(state:action:)`` 13 | 14 | ### Combine integration 15 | 16 | - ``StorePublisher`` 17 | 18 | ### UIKit integration 19 | 20 | - ``ifLet(then:else:)`` 21 | 22 | ### Deprecations 23 | 24 | - 25 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/UIKit.md: -------------------------------------------------------------------------------- 1 | # UIKit Integration 2 | 3 | Integrating the Composable Architecture into a UIKit application. 4 | 5 | ## Overview 6 | 7 | While the Composable Architecture was designed with SwiftUI in mind, it comes with tools to integrate into application code written in UIKit. 8 | 9 | ## Topics 10 | 11 | ### Scoping stores 12 | 13 | - ``Store/ifLet(then:else:)`` 14 | 15 | ### Subscribing to state changes 16 | 17 | - ``ViewStore/publisher`` 18 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/TypeName.swift: -------------------------------------------------------------------------------- 1 | @usableFromInline 2 | func typeName(_ type: Any.Type) -> String { 3 | var name = _typeName(type, qualified: true) 4 | if let index = name.firstIndex(of: ".") { 5 | name.removeSubrange(...index) 6 | } 7 | let sanitizedName = 8 | name 9 | .replacingOccurrences( 10 | of: #"<.+>|\(unknown context at \$[[:xdigit:]]+\)\."#, 11 | with: "", 12 | options: .regularExpression 13 | ) 14 | return sanitizedName 15 | } 16 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Internal/UIViewRepresented.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct UIViewRepresented: UIViewRepresentable { 4 | let makeUIView: (Context) -> UIViewType 5 | let updateUIView: (UIViewType, Context) -> Void = { _, _ in } 6 | 7 | func makeUIView(context: Context) -> UIViewType { 8 | self.makeUIView(context) 9 | } 10 | 11 | func updateUIView(_ uiView: UIViewType, context: Context) { 12 | self.updateUIView(uiView, context) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Internal/UIViewRepresented.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct UIViewRepresented: UIViewRepresentable { 4 | let makeUIView: (Context) -> UIViewType 5 | let updateUIView: (UIViewType, Context) -> Void = { _, _ in } 6 | 7 | func makeUIView(context: Context) -> UIViewType { 8 | self.makeUIView(context) 9 | } 10 | 11 | func updateUIView(_ uiView: UIViewType, context: Context) { 12 | self.updateUIView(uiView, context) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/ComposableArchitectureTests/SerialExecutor.swift: -------------------------------------------------------------------------------- 1 | import _CAsyncSupport 2 | 3 | @_spi(Internals) public func _withMainSerialExecutor( 4 | @_implicitSelfCapture operation: () async throws -> T 5 | ) async rethrows -> T { 6 | let hook = swift_task_enqueueGlobal_hook 7 | defer { swift_task_enqueueGlobal_hook = hook } 8 | swift_task_enqueueGlobal_hook = { job, original in 9 | MainActor.shared.enqueue(unsafeBitCast(job, to: UnownedJob.self)) 10 | } 11 | return try await operation() 12 | } 13 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/README.md: -------------------------------------------------------------------------------- 1 | # Voice Memos 2 | 3 | This application demonstrates how to work with multiple dependencies and manage a complex state machine driven off of timers in the Composable Architecture. Some functionality includes: 4 | 5 | * Requesting the user’s permission to record audio. 6 | * Prompting the user if insufficient permission is provided. 7 | * Audio recording and playback. 8 | * Handling errors that may occur during recording or playback. 9 | * Stubbing dependencies to work with SwiftUI previews. 10 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerBuilder.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/ReducerBuilder`` 2 | 3 | ## Topics 4 | 5 | ### Building reducers 6 | 7 | - ``ReducerBuilderOf`` 8 | - ``buildExpression(_:)`` 9 | - ``buildBlock(_:)`` 10 | - ``buildPartialBlock(first:)`` 11 | - ``buildPartialBlock(accumulated:next:)`` 12 | - ``buildOptional(_:)`` 13 | - ``buildEither(first:)`` 14 | - ``buildEither(second:)`` 15 | - ``buildArray(_:)`` 16 | - ``buildLimitedAvailability(_:)`` 17 | - ``buildBlock()`` 18 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/Locking.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | extension UnsafeMutablePointer where Pointee == os_unfair_lock_s { 4 | @inlinable @discardableResult 5 | func sync(_ work: () -> R) -> R { 6 | os_unfair_lock_lock(self) 7 | defer { os_unfair_lock_unlock(self) } 8 | return work() 9 | } 10 | } 11 | 12 | extension NSRecursiveLock { 13 | @inlinable @discardableResult 14 | @_spi(Internals) public func sync(work: () -> R) -> R { 15 | self.lock() 16 | defer { self.unlock() } 17 | return work() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Examples/Integration/IntegrationUITests/NavigationStackBindingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @MainActor 4 | final class NavigationStackBindingTests: XCTestCase { 5 | override func setUpWithError() throws { 6 | self.continueAfterFailure = false 7 | } 8 | 9 | func testExample() async throws { 10 | let app = XCUIApplication() 11 | app.launch() 12 | app.collectionViews.buttons["NavigationStackBindingTestCase"].tap() 13 | app.buttons["Go to child"].tap() 14 | app.buttons["Back"].tap() 15 | XCTAssertTrue(app.buttons["Go to child"].exists) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/StoreDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | Review unsupported store APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. 8 | 9 | ## Topics 10 | 11 | ### Creating a store 12 | 13 | - ``Store/init(initialState:reducer:environment:)`` 14 | 15 | ### Scoping stores 16 | 17 | - ``Store/publisherScope(state:action:)`` 18 | - ``Store/publisherScope(state:)`` 19 | - ``Store/unchecked(initialState:reducer:environment:)`` 20 | -------------------------------------------------------------------------------- /Sources/swift-composable-architecture-benchmark/ViewStore.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import Combine 3 | import ComposableArchitecture 4 | import Foundation 5 | 6 | let viewStoreSuite = BenchmarkSuite(name: "ViewStore") { 7 | let store = Store( 8 | initialState: 0, 9 | reducer: EmptyReducer() 10 | ) 11 | 12 | $0.benchmark("Create view store to send action") { 13 | doNotOptimizeAway(ViewStore(store).send(())) 14 | } 15 | 16 | let viewStore = ViewStore(store) 17 | 18 | $0.benchmark("Send action to pre-created view store") { 19 | doNotOptimizeAway(viewStore.send(())) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Reducer/Reducers/EmptyReducer.swift: -------------------------------------------------------------------------------- 1 | /// A reducer that does nothing. 2 | /// 3 | /// While not very useful on its own, `EmptyReducer` can be used as a placeholder in APIs that hold 4 | /// reducers. 5 | public struct EmptyReducer: ReducerProtocol { 6 | /// Initializes a reducer that does nothing. 7 | @inlinable 8 | public init() { 9 | self.init(internal: ()) 10 | } 11 | 12 | @usableFromInline 13 | init(internal: Void) {} 14 | 15 | @inlinable 16 | public func reduce(into _: inout State, action _: Action) -> EffectTask { 17 | .none 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Reducer/Reducers/Optional.swift: -------------------------------------------------------------------------------- 1 | extension Optional: ReducerProtocol where Wrapped: ReducerProtocol { 2 | #if swift(<5.7) 3 | public typealias State = Wrapped.State 4 | public typealias Action = Wrapped.Action 5 | public typealias _Body = Never 6 | #endif 7 | 8 | @inlinable 9 | public func reduce( 10 | into state: inout Wrapped.State, action: Wrapped.Action 11 | ) -> EffectTask { 12 | switch self { 13 | case let .some(wrapped): 14 | return wrapped.reduce(into: &state, action: action) 15 | case .none: 16 | return .none 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/TaskCancellableValue.swift: -------------------------------------------------------------------------------- 1 | extension Task where Failure == Error { 2 | @_spi(Internals) public var cancellableValue: Success { 3 | get async throws { 4 | try await withTaskCancellationHandler { 5 | try await self.value 6 | } onCancel: { 7 | self.cancel() 8 | } 9 | } 10 | } 11 | } 12 | 13 | extension Task where Failure == Never { 14 | @usableFromInline 15 | var cancellableValue: Success { 16 | get async { 17 | await withTaskCancellationHandler { 18 | await self.value 19 | } onCancel: { 20 | self.cancel() 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/SwiftUIDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | Review unsupported SwiftUI APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. 8 | 9 | ## Topics 10 | 11 | ### ActionSheetState 12 | 13 | - ``ActionSheetState`` 14 | 15 | ### BindableState 16 | 17 | - ``BindableState`` 18 | 19 | ### ForEachStore 20 | 21 | - ``ForEachStore/init(_:content:)-34mtj`` 22 | - ``ForEachStore/init(_:id:content:)`` 23 | 24 | ### WithViewStore 25 | 26 | - ``WithViewStore/Action`` 27 | - ``WithViewStore/State`` 28 | -------------------------------------------------------------------------------- /Examples/SpeechRecognition/README.md: -------------------------------------------------------------------------------- 1 | # Speech Recognition 2 | 3 | This application demonstrates how to work with a complex dependency in the Composable Architecture. It uses the `SFSpeechRecognizer` API from the `Speech` framework to listen to audio on the device and live-transcribe it to the UI. 4 | 5 | The `SFSpeechRecognizer` class is a complex dependency, and if we used it freely in our application we wouldn't be able to test any of that code. So, instead, we wrap the API in a `SpeechClient` type that exposes asynchronous endpoints for accessing the underlying `SFSpeechRecognizer` class. Then we can use it in the reducer in an understandable way, _and_ we can write tests for the reducer. 6 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/ViewStoreDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | Review unsupported view store APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. 8 | 9 | ## Topics 10 | 11 | ### Creating a view store 12 | 13 | - ``ViewStore/init(_:removeDuplicates:)`` 14 | - ``ViewStore/init(_:)-1pfeq`` 15 | 16 | ### Interacting with Concurrency 17 | 18 | - ``ViewStore/suspend(while:)`` 19 | 20 | ### SwiftUI integration 21 | 22 | - ``ViewStore/subscript(dynamicMember:)-3q4xh`` 23 | - ``ViewStore/binding(keyPath:send:)`` 24 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Internal/CircularProgressView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | struct CircularProgressView: View { 4 | private let value: Double 5 | 6 | init(value: Double) { 7 | self.value = value 8 | } 9 | 10 | var body: some View { 11 | Circle() 12 | .trim(from: 0, to: CGFloat(self.value)) 13 | .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round)) 14 | .rotationEffect(.degrees(-90)) 15 | .animation(.easeIn, value: self.value) 16 | } 17 | } 18 | 19 | struct CircularProgressView_Previews: PreviewProvider { 20 | static var previews: some View { 21 | CircularProgressView(value: 0.3).frame(width: 44, height: 44) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/ViewStore.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/ViewStore`` 2 | 3 | ## Topics 4 | 5 | ### Creating a view store 6 | 7 | - ``init(_:observe:removeDuplicates:)`` 8 | - ``init(_:observe:)`` 9 | - ``init(_:)-4il0f`` 10 | - ``ViewStoreOf`` 11 | 12 | ### Accessing state 13 | 14 | - ``state-swift.property`` 15 | - ``subscript(dynamicMember:)-kwxk`` 16 | 17 | ### Sending actions 18 | 19 | - ``send(_:)`` 20 | - ``send(_:while:)`` 21 | - ``yield(while:)`` 22 | - ``ViewStoreTask`` 23 | 24 | ### SwiftUI integration 25 | 26 | - ``send(_:animation:)`` 27 | - ``send(_:animation:while:)`` 28 | - 29 | - ``objectWillChange-5oies`` 30 | 31 | ### Deprecations 32 | 33 | - 34 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/AudioPlayerClient.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import Foundation 3 | import XCTestDynamicOverlay 4 | 5 | struct AudioPlayerClient { 6 | var play: @Sendable (URL) async throws -> Bool 7 | } 8 | 9 | extension AudioPlayerClient: TestDependencyKey { 10 | static let previewValue = Self( 11 | play: { _ in 12 | try await Task.sleep(nanoseconds: NSEC_PER_SEC * 5) 13 | return true 14 | } 15 | ) 16 | 17 | static let testValue = Self( 18 | play: unimplemented("\(Self.self).play") 19 | ) 20 | } 21 | 22 | extension DependencyValues { 23 | var audioPlayer: AudioPlayerClient { 24 | get { self[AudioPlayerClient.self] } 25 | set { self[AudioPlayerClient.self] = newValue } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | 3 | contact_links: 4 | - name: Project Discussion 5 | url: https://github.com/pointfreeco/swift-composable-architecture/discussions 6 | about: Composable Architecture Q&A, ideas, and more 7 | - name: Documentation 8 | url: https://pointfreeco.github.io/swift-composable-architecture/main/documentation/composablearchitecture/ 9 | about: Read the Composable Architecture's documentation 10 | - name: Videos 11 | url: https://www.pointfree.co/collections/composable-architecture 12 | about: Watch videos to get a behind-the-scenes look at how the Composable Architecture was motivated and built 13 | - name: Slack 14 | url: https://www.pointfree.co/slack-invite 15 | about: Community chat 16 | -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudies/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | import UIKit 4 | 5 | @UIApplicationMain 6 | class AppDelegate: UIResponder, UIApplicationDelegate { 7 | var window: UIWindow? 8 | 9 | func application( 10 | _ application: UIApplication, 11 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 12 | ) -> Bool { 13 | let contentView = RootView( 14 | store: Store( 15 | initialState: Root.State(), 16 | reducer: Root() 17 | ) 18 | ) 19 | 20 | let window = UIWindow(frame: UIScreen.main.bounds) 21 | window.rootViewController = UIHostingController(rootView: contentView) 22 | self.window = window 23 | window.makeKeyAndVisible() 24 | return true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | 3 | final class ActivityIndicatorViewController: UIViewController { 4 | override func viewDidLoad() { 5 | super.viewDidLoad() 6 | 7 | self.view.backgroundColor = .systemBackground 8 | 9 | let activityIndicator = UIActivityIndicatorView() 10 | activityIndicator.startAnimating() 11 | activityIndicator.translatesAutoresizingMaskIntoConstraints = false 12 | self.view.addSubview(activityIndicator) 13 | 14 | NSLayoutConstraint.activate([ 15 | activityIndicator.centerXAnchor.constraint( 16 | equalTo: self.view.safeAreaLayoutGuide.centerXAnchor), 17 | activityIndicator.centerYAnchor.constraint( 18 | equalTo: self.view.safeAreaLayoutGuide.centerYAnchor), 19 | ]) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Reducer/Reducers/BindingReducer.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | /// A reducer that updates bindable state when it receives binding actions. 4 | public struct BindingReducer: ReducerProtocol 5 | where Action: BindableAction, State == Action.State { 6 | /// Initializes a reducer that updates bindable state when it receives binding actions. 7 | @inlinable 8 | public init() { 9 | self.init(internal: ()) 10 | } 11 | 12 | @usableFromInline 13 | init(internal: Void) {} 14 | 15 | @inlinable 16 | public func reduce( 17 | into state: inout State, action: Action 18 | ) -> EffectTask { 19 | guard let bindingAction = (/Action.binding).extract(from: action) 20 | else { return .none } 21 | 22 | bindingAction.set(&state) 23 | return .none 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Tests/ComposableArchitectureTests/StoreFilterTests.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import Combine 3 | import XCTest 4 | 5 | @testable import ComposableArchitecture 6 | 7 | @MainActor 8 | final class StoreFilterTests: XCTestCase { 9 | var cancellables: Set = [] 10 | 11 | func testFilter() { 12 | let store = Store(initialState: nil, reducer: EmptyReducer()) 13 | .filter { state, _ in state != nil } 14 | 15 | let viewStore = ViewStore(store) 16 | var count = 0 17 | viewStore.publisher 18 | .sink { _ in count += 1 } 19 | .store(in: &self.cancellables) 20 | 21 | XCTAssertEqual(count, 1) 22 | viewStore.send(()) 23 | XCTAssertEqual(count, 1) 24 | viewStore.send(()) 25 | XCTAssertEqual(count, 1) 26 | } 27 | } 28 | #endif 29 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Binding { 4 | /// SwiftUI will print errors to the console about "AttributeGraph: cycle detected" if you disable 5 | /// a text field while it is focused. This hack will force all fields to unfocus before we write 6 | /// to a binding that may disable the fields. 7 | /// 8 | /// See also: https://stackoverflow.com/a/69653555 9 | @MainActor 10 | func resignFirstResponder() -> Self { 11 | Self( 12 | get: { self.wrappedValue }, 13 | set: { newValue, transaction in 14 | UIApplication.shared.sendAction( 15 | #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil 16 | ) 17 | self.transaction(transaction).wrappedValue = newValue 18 | } 19 | ) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudiesTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | concurrency: 9 | group: format-${{ github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | swift_format: 14 | name: swift-format 15 | runs-on: macos-12 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Xcode Select 19 | run: sudo xcode-select -s /Applications/Xcode_14.0.1.app 20 | - name: Tap 21 | run: brew tap pointfreeco/formulae 22 | - name: Install 23 | run: brew install Formulae/swift-format@5.7 24 | - name: Format 25 | run: make format 26 | - uses: stefanzweifel/git-auto-commit-action@v4 27 | with: 28 | commit_message: Run swift-format 29 | branch: 'main' 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import UIKit 3 | 4 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 5 | var window: UIWindow? 6 | 7 | func scene( 8 | _ scene: UIScene, 9 | willConnectTo session: UISceneSession, 10 | options connectionOptions: UIScene.ConnectionOptions 11 | ) { 12 | self.window = (scene as? UIWindowScene).map { UIWindow(windowScene: $0) } 13 | self.window?.rootViewController = UINavigationController( 14 | rootViewController: RootViewController()) 15 | self.window?.makeKeyAndVisible() 16 | } 17 | } 18 | 19 | @UIApplicationMain 20 | class AppDelegate: UIResponder, UIApplicationDelegate { 21 | func application( 22 | _ application: UIApplication, 23 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 24 | ) -> Bool { 25 | true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/Sources/AppSwiftUI/AppView.swift: -------------------------------------------------------------------------------- 1 | import AppCore 2 | import ComposableArchitecture 3 | import LoginSwiftUI 4 | import NewGameSwiftUI 5 | import SwiftUI 6 | 7 | public struct AppView: View { 8 | let store: StoreOf 9 | 10 | public init(store: StoreOf) { 11 | self.store = store 12 | } 13 | 14 | public var body: some View { 15 | SwitchStore(self.store) { 16 | CaseLet(state: /TicTacToe.State.login, action: TicTacToe.Action.login) { store in 17 | NavigationView { 18 | LoginView(store: store) 19 | } 20 | .navigationViewStyle(.stack) 21 | } 22 | CaseLet(state: /TicTacToe.State.newGame, action: TicTacToe.Action.newGame) { store in 23 | NavigationView { 24 | NewGameView(store: store) 25 | } 26 | .navigationViewStyle(.stack) 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/swift-composable-architecture-benchmark/StoreScope.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import ComposableArchitecture 3 | 4 | private struct Counter: ReducerProtocol { 5 | typealias State = Int 6 | typealias Action = Bool 7 | func reduce(into state: inout Int, action: Bool) -> EffectTask { 8 | if action { 9 | state += 1 10 | return .none 11 | } else { 12 | state -= 1 13 | return .none 14 | } 15 | } 16 | } 17 | 18 | let storeScopeSuite = BenchmarkSuite(name: "Store scoping") { suite in 19 | var store = Store(initialState: 0, reducer: Counter()) 20 | var viewStores: [ViewStore] = [ViewStore(store)] 21 | for _ in 1...4 { 22 | store = store.scope(state: { $0 }) 23 | viewStores.append(ViewStore(store)) 24 | } 25 | let lastViewStore = viewStores.last! 26 | 27 | suite.benchmark("Nested store") { 28 | lastViewStore.send(true) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Sources/swift-composable-architecture-benchmark/Effects.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import Combine 3 | import ComposableArchitecture 4 | import Foundation 5 | 6 | let effectSuite = BenchmarkSuite(name: "Effects") { 7 | $0.benchmark("Merged Effect.none (create, flat)") { 8 | doNotOptimizeAway(EffectTask.merge((1...100).map { _ in .none })) 9 | } 10 | 11 | $0.benchmark("Merged Effect.none (create, nested)") { 12 | var effect = EffectTask.none 13 | for _ in 1...100 { 14 | effect = effect.merge(with: .none) 15 | } 16 | doNotOptimizeAway(effect) 17 | } 18 | 19 | let effect = EffectTask.merge((1...100).map { _ in .none }) 20 | var didComplete = false 21 | $0.benchmark("Merged Effect.none (sink)") { 22 | doNotOptimizeAway( 23 | effect.sink(receiveCompletion: { _ in didComplete = true }, receiveValue: { _ in }) 24 | ) 25 | } tearDown: { 26 | precondition(didComplete) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ComposableArchitecture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/SwiftUI.md: -------------------------------------------------------------------------------- 1 | # SwiftUI Integration 2 | 3 | Integrating the Composable Architecture into a SwiftUI application. 4 | 5 | ## Overview 6 | 7 | The Composable Architecture can be used to power applications built in many frameworks, but it was designed with SwiftUI in mind, and comes with many powerful tools to integrate into your SwiftUI applications. 8 | 9 | ## Topics 10 | 11 | ### View containers 12 | 13 | - ``WithViewStore`` 14 | - ``IfLetStore`` 15 | - ``ForEachStore`` 16 | - ``SwitchStore`` 17 | 18 | ### Bindings 19 | 20 | - 21 | - ``ViewStore/binding(get:send:)-65xes`` 22 | - ``BindingState`` 23 | - ``BindableAction`` 24 | - ``BindingAction`` 25 | - ``BindingReducer`` 26 | - ``ViewStore/binding(_:file:fileID:line:)`` 27 | 28 | 29 | 30 | 31 | ### Deprecations 32 | 33 | - 34 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTest 3 | 4 | @testable import SwiftUICaseStudies 5 | 6 | @MainActor 7 | final class BindingFormTests: XCTestCase { 8 | func testBasics() async { 9 | let store = TestStore( 10 | initialState: BindingForm.State(), 11 | reducer: BindingForm() 12 | ) 13 | 14 | await store.send(.set(\.$sliderValue, 2)) { 15 | $0.sliderValue = 2 16 | } 17 | await store.send(.set(\.$stepCount, 1)) { 18 | $0.sliderValue = 1 19 | $0.stepCount = 1 20 | } 21 | await store.send(.set(\.$text, "Blob")) { 22 | $0.text = "Blob" 23 | } 24 | await store.send(.set(\.$toggleIsOn, true)) { 25 | $0.toggleIsOn = true 26 | } 27 | await store.send(.resetButtonTapped) { 28 | $0 = BindingForm.State(sliderValue: 5, stepCount: 10, text: "", toggleIsOn: false) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/ReducerProtocol.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/ReducerProtocol`` 2 | 3 | ## Topics 4 | 5 | ### Implementing a reducer 6 | 7 | - ``reduce(into:action:)-8yinq`` 8 | - ``State`` 9 | - ``Action`` 10 | - ``EffectTask`` 11 | 12 | ### Reducer composition 13 | 14 | - ``body-swift.property-97ymy`` 15 | - ``Body-swift.typealias`` 16 | - ``ReducerBuilder`` 17 | - ``Scope`` 18 | - ``ifLet(_:action:then:file:fileID:line:)`` 19 | - ``ifCaseLet(_:action:then:file:fileID:line:)`` 20 | - ``forEach(_:action:element:file:fileID:line:)`` 21 | 22 | ### Supporting reducers 23 | 24 | - ``Reduce`` 25 | - ``CombineReducers`` 26 | - ``EmptyReducer`` 27 | - ``BindingReducer`` 28 | 29 | ### Reducer modifiers 30 | 31 | - ``dependency(_:_:)`` 32 | - ``transformDependency(_:transform:)`` 33 | - ``signpost(_:log:)`` 34 | 35 | ### Supporting types 36 | 37 | - ``ReducerProtocolOf`` 38 | 39 | ### Deprecations 40 | 41 | - 42 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-LongLivingTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTest 3 | 4 | @testable import SwiftUICaseStudies 5 | 6 | @MainActor 7 | final class LongLivingEffectsTests: XCTestCase { 8 | func testReducer() async { 9 | let (screenshots, takeScreenshot) = AsyncStream.streamWithContinuation() 10 | 11 | let store = TestStore( 12 | initialState: LongLivingEffects.State(), 13 | reducer: LongLivingEffects() 14 | ) { 15 | $0.screenshots = { screenshots } 16 | } 17 | 18 | let task = await store.send(.task) 19 | 20 | // Simulate a screenshot being taken 21 | takeScreenshot.yield() 22 | 23 | await store.receive(.userDidTakeScreenshotNotification) { 24 | $0.screenshotCount = 1 25 | } 26 | 27 | // Simulate screen going away 28 | await task.cancel() 29 | 30 | // Simulate a screenshot being taken to show no effects are executed. 31 | takeScreenshot.yield() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudies/RootView.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct RootView: View { 5 | let store: StoreOf 6 | 7 | var body: some View { 8 | NavigationView { 9 | Form { 10 | Section { 11 | self.focusView 12 | } 13 | } 14 | } 15 | } 16 | 17 | var focusView: AnyView? { 18 | if #available(tvOS 14.0, *) { 19 | return AnyView( 20 | NavigationLink( 21 | "Focus", 22 | destination: FocusView( 23 | store: self.store.scope(state: \.focus, action: Root.Action.focus) 24 | ) 25 | ) 26 | ) 27 | } else { 28 | return nil 29 | } 30 | } 31 | } 32 | 33 | struct ContentView_Previews: PreviewProvider { 34 | static var previews: some View { 35 | NavigationView { 36 | RootView( 37 | store: Store( 38 | initialState: Root.State(), 39 | reducer: Root() 40 | ) 41 | ) 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import NewGameCore 3 | import XCTest 4 | 5 | @testable import NewGameSwiftUI 6 | 7 | @MainActor 8 | final class NewGameSwiftUITests: XCTestCase { 9 | let store = TestStore( 10 | initialState: NewGame.State(), 11 | reducer: NewGame(), 12 | observe: NewGameView.ViewState.init, 13 | send: NewGame.Action.init 14 | ) 15 | 16 | func testNewGame() async { 17 | await self.store.send(.xPlayerNameChanged("Blob Sr.")) { 18 | $0.xPlayerName = "Blob Sr." 19 | } 20 | await self.store.send(.oPlayerNameChanged("Blob Jr.")) { 21 | $0.oPlayerName = "Blob Jr." 22 | $0.isLetsPlayButtonDisabled = false 23 | } 24 | await self.store.send(.letsPlayButtonTapped) { 25 | $0.isGameActive = true 26 | } 27 | await self.store.send(.gameDismissed) { 28 | $0.isGameActive = false 29 | } 30 | await self.store.send(.logoutButtonTapped) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Examples/Integration/IntegrationUITests/ForEachBindingTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @MainActor 4 | final class ForEachBindingTests: XCTestCase { 5 | override func setUpWithError() throws { 6 | self.continueAfterFailure = false 7 | } 8 | 9 | func testExample() async throws { 10 | let app = XCUIApplication() 11 | app.launch() 12 | 13 | app.collectionViews.buttons["ForEachBindingTestCase"].tap() 14 | app.buttons["Remove last"].tap() 15 | XCTAssertFalse(app.textFields["C"].exists) 16 | app.buttons["Remove last"].tap() 17 | XCTAssertFalse(app.textFields["B"].exists) 18 | app.buttons["Remove last"].tap() 19 | XCTAssertFalse(app.textFields["A"].exists) 20 | 21 | XCTExpectFailure( 22 | """ 23 | This ideally would not fail, but currently does. See this PR for more details: 24 | 25 | https://github.com/pointfreeco/swift-composable-architecture/pull/1845 26 | """ 27 | ) { 28 | XCTAssertFalse(app.staticTexts["🛑"].exists) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudiesTests/FocusTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTest 3 | 4 | @testable import tvOSCaseStudies 5 | 6 | @MainActor 7 | final class tvOSCaseStudiesTests: XCTestCase { 8 | func testFocus() async { 9 | let store = TestStore( 10 | initialState: Focus.State(currentFocus: 1), 11 | reducer: Focus() 12 | ) { 13 | $0.withRandomNumberGenerator = .init(LCRNG()) 14 | } 15 | 16 | await store.send(.randomButtonClicked) 17 | await store.send(.randomButtonClicked) { 18 | $0.currentFocus = 4 19 | } 20 | await store.send(.randomButtonClicked) { 21 | $0.currentFocus = 9 22 | } 23 | } 24 | } 25 | 26 | /// A linear congruential random number generator. 27 | struct LCRNG: RandomNumberGenerator { 28 | var seed: UInt64 29 | 30 | init(seed: UInt64 = 0) { 31 | self.seed = seed 32 | } 33 | 34 | mutating func next() -> UInt64 { 35 | self.seed = 2_862_933_555_777_941_757 &* self.seed &+ 3_037_000_493 36 | return self.seed 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClientLive/LiveAuthenticationClient.swift: -------------------------------------------------------------------------------- 1 | import AuthenticationClient 2 | import Dependencies 3 | import Foundation 4 | 5 | extension AuthenticationClient: DependencyKey { 6 | public static let liveValue = Self( 7 | login: { request in 8 | guard request.email.contains("@") && request.password == "password" 9 | else { throw AuthenticationError.invalidUserPassword } 10 | 11 | try await Task.sleep(nanoseconds: NSEC_PER_SEC) 12 | return AuthenticationResponse( 13 | token: "deadbeef", twoFactorRequired: request.email.contains("2fa") 14 | ) 15 | }, 16 | twoFactor: { request in 17 | guard request.token == "deadbeef" 18 | else { throw AuthenticationError.invalidIntermediateToken } 19 | 20 | guard request.code == "1234" 21 | else { throw AuthenticationError.invalidTwoFactor } 22 | 23 | try await Task.sleep(nanoseconds: NSEC_PER_SEC) 24 | return AuthenticationResponse(token: "deadbeefdeadbeef", twoFactorRequired: false) 25 | } 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /Examples/Search/README.md: -------------------------------------------------------------------------------- 1 | # Search 2 | 3 | This application demonstrates how to build a moderately complex search feature in the Composable Architecture: 4 | 5 | * Typing into the search field executes an API request to search for locations. 6 | * Tapping a location runs another API request to fetch the weather for that location, and when a response is received the data is displayed inline in that row. 7 | 8 | In addition to those basic features, the following extra things are implemented: 9 | 10 | * Search API requests are debounced so that one is run only after the user stops typing for 300ms. 11 | * If you tap a location while a weather API request is already in-flight it will cancel that request and start a new one. 12 | * Dependencies and side effects are fully controlled. The reducer that runs this application needs a [weather API client](Search/WeatherClient.swift) to run effects. 13 | * A full [test suite](SearchTests/SearchTests.swift) is implemented. Not only is core functionality tested, but also failure flows and subtle edge cases (e.g. clearing the search query cancels any in-flight search requests). 14 | -------------------------------------------------------------------------------- /Examples/CaseStudies/tvOSCaseStudies/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | arm64 28 | 29 | UIUserInterfaceStyle 30 | Automatic 31 | 32 | 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Point-Free, Inc. 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 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Effect.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/EffectTask`` 2 | 3 | ## Topics 4 | 5 | ### Creating an effect 6 | 7 | - ``EffectPublisher/none`` 8 | - ``EffectPublisher/task(priority:operation:catch:file:fileID:line:)`` 9 | - ``EffectPublisher/run(priority:operation:catch:file:fileID:line:)`` 10 | - ``EffectPublisher/fireAndForget(priority:_:)`` 11 | - ``EffectPublisher/send(_:)`` 12 | - ``TaskResult`` 13 | 14 | ### Cancellation 15 | 16 | - ``EffectPublisher/cancellable(id:cancelInFlight:)-29q60`` 17 | - ``EffectPublisher/cancel(id:)-6hzsl`` 18 | - ``EffectPublisher/cancel(ids:)-1cqqx`` 19 | - ``withTaskCancellation(id:cancelInFlight:operation:)-4dtr6`` 20 | 21 | ### Composition 22 | 23 | - ``EffectPublisher/map(_:)-yn70`` 24 | - ``EffectPublisher/merge(_:)-45guh`` 25 | - ``EffectPublisher/merge(_:)-3d54p`` 26 | 27 | ### Testing 28 | 29 | - ``EffectPublisher/unimplemented(_:)`` 30 | 31 | ### Combine integration 32 | 33 | - ``EffectPublisher/publisher(_:)`` 34 | 35 | ### SwiftUI integration 36 | 37 | - ``EffectPublisher/animation(_:)`` 38 | 39 | ### Deprecations 40 | 41 | - 42 | -------------------------------------------------------------------------------- /Tests/ComposableArchitectureTests/DeprecatedTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTest 3 | 4 | @available(*, deprecated) 5 | final class DeprecatedTests: XCTestCase { 6 | func testUncheckedStore() { 7 | var expectations: [XCTestExpectation] = [] 8 | for n in 1...100 { 9 | let expectation = XCTestExpectation(description: "\(n)th iteration is complete") 10 | expectations.append(expectation) 11 | DispatchQueue.global().async { 12 | let viewStore = ViewStore( 13 | Store.unchecked( 14 | initialState: 0, 15 | reducer: AnyReducer { state, _, expectation in 16 | state += 1 17 | if state == 2 { 18 | return .fireAndForget { expectation.fulfill() } 19 | } 20 | return .none 21 | }, 22 | environment: expectation 23 | ) 24 | ) 25 | viewStore.send(()) 26 | DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { 27 | viewStore.send(()) 28 | } 29 | } 30 | } 31 | 32 | wait(for: expectations, timeout: 1) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Internal/OpenExistential.swift: -------------------------------------------------------------------------------- 1 | #if swift(>=5.7) 2 | // MARK: swift(>=5.7) 3 | // MARK: Equatable 4 | 5 | func _isEqual(_ lhs: Any, _ rhs: Any) -> Bool? { 6 | (lhs as? any Equatable)?.isEqual(other: rhs) 7 | } 8 | 9 | extension Equatable { 10 | fileprivate func isEqual(other: Any) -> Bool { 11 | self == other as? Self 12 | } 13 | } 14 | #else 15 | // MARK: - 16 | // MARK: swift(<5.7) 17 | 18 | private enum Witness {} 19 | 20 | // MARK: Equatable 21 | 22 | func _isEqual(_ lhs: Any, _ rhs: Any) -> Bool? { 23 | func open(_: T.Type) -> Bool? { 24 | (Witness.self as? AnyEquatable.Type)?.isEqual(lhs, rhs) 25 | } 26 | return _openExistential(type(of: lhs), do: open) 27 | } 28 | 29 | private protocol AnyEquatable { 30 | static func isEqual(_ lhs: Any, _ rhs: Any) -> Bool 31 | } 32 | 33 | extension Witness: AnyEquatable where T: Equatable { 34 | fileprivate static func isEqual(_ lhs: Any, _ rhs: Any) -> Bool { 35 | guard 36 | let lhs = lhs as? T, 37 | let rhs = rhs as? T 38 | else { return false } 39 | return lhs == rhs 40 | } 41 | } 42 | #endif 43 | -------------------------------------------------------------------------------- /Tests/ComposableArchitectureTests/BindingLocalTests.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import XCTest 3 | 4 | @testable import ComposableArchitecture 5 | 6 | @MainActor 7 | final class BindingLocalTests: XCTestCase { 8 | public func testBindingLocalIsActive() { 9 | XCTAssertFalse(BindingLocal.isActive) 10 | 11 | struct MyReducer: ReducerProtocol { 12 | struct State: Equatable { 13 | var text = "" 14 | } 15 | 16 | enum Action: Equatable { 17 | case textChanged(String) 18 | } 19 | 20 | func reduce(into state: inout State, action: Action) -> EffectTask { 21 | switch action { 22 | case let .textChanged(text): 23 | state.text = text 24 | return .none 25 | } 26 | } 27 | } 28 | 29 | let store = Store(initialState: MyReducer.State(), reducer: MyReducer()) 30 | let viewStore = ViewStore(store, observe: { $0 }) 31 | 32 | let binding = viewStore.binding(get: \.text) { text in 33 | XCTAssertTrue(BindingLocal.isActive) 34 | return .textChanged(text) 35 | } 36 | binding.wrappedValue = "Hello!" 37 | XCTAssertEqual(viewStore.text, "Hello!") 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/FactClient.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import Foundation 3 | import XCTestDynamicOverlay 4 | 5 | struct FactClient { 6 | var fetch: @Sendable (Int) async throws -> String 7 | } 8 | 9 | extension DependencyValues { 10 | var factClient: FactClient { 11 | get { self[FactClient.self] } 12 | set { self[FactClient.self] = newValue } 13 | } 14 | } 15 | 16 | extension FactClient: DependencyKey { 17 | /// This is the "live" fact dependency that reaches into the outside world to fetch trivia. 18 | /// Typically this live implementation of the dependency would live in its own module so that the 19 | /// main feature doesn't need to compile it. 20 | static let liveValue = Self( 21 | fetch: { number in 22 | try await Task.sleep(nanoseconds: NSEC_PER_SEC) 23 | let (data, _) = try await URLSession.shared 24 | .data(from: URL(string: "http://numbersapi.com/\(number)/trivia")!) 25 | return String(decoding: data, as: UTF8.self) 26 | } 27 | ) 28 | 29 | /// This is the "unimplemented" fact dependency that is useful to plug into tests that you want 30 | /// to prove do not need the dependency. 31 | static let testValue = Self( 32 | fetch: unimplemented("\(Self.self).fetch") 33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-TimersTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTest 3 | 4 | @testable import SwiftUICaseStudies 5 | 6 | @MainActor 7 | final class TimersTests: XCTestCase { 8 | func testStart() async { 9 | let clock = TestClock() 10 | 11 | let store = TestStore( 12 | initialState: Timers.State(), 13 | reducer: Timers() 14 | ) { 15 | $0.continuousClock = clock 16 | } 17 | 18 | await store.send(.toggleTimerButtonTapped) { 19 | $0.isTimerActive = true 20 | } 21 | await clock.advance(by: .seconds(1)) 22 | await store.receive(.timerTicked) { 23 | $0.secondsElapsed = 1 24 | } 25 | await clock.advance(by: .seconds(5)) 26 | await store.receive(.timerTicked) { 27 | $0.secondsElapsed = 2 28 | } 29 | await store.receive(.timerTicked) { 30 | $0.secondsElapsed = 3 31 | } 32 | await store.receive(.timerTicked) { 33 | $0.secondsElapsed = 4 34 | } 35 | await store.receive(.timerTicked) { 36 | $0.secondsElapsed = 5 37 | } 38 | await store.receive(.timerTicked) { 39 | $0.secondsElapsed = 6 40 | } 41 | await store.send(.toggleTimerButtonTapped) { 42 | $0.isTimerActive = false 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import XCTest 3 | 4 | @testable import SwiftUICaseStudies 5 | 6 | @MainActor 7 | final class LifecycleTests: XCTestCase { 8 | func testLifecycle() async { 9 | let clock = TestClock() 10 | 11 | let store = TestStore( 12 | initialState: LifecycleDemo.State(), 13 | reducer: LifecycleDemo() 14 | ) { 15 | $0.continuousClock = clock 16 | } 17 | 18 | await store.send(.toggleTimerButtonTapped) { 19 | $0.count = 0 20 | } 21 | 22 | await store.send(.timer(.onAppear)) 23 | 24 | await clock.advance(by: .seconds(1)) 25 | await store.receive(.timer(.wrapped(.tick))) { 26 | $0.count = 1 27 | } 28 | 29 | await clock.advance(by: .seconds(1)) 30 | await store.receive(.timer(.wrapped(.tick))) { 31 | $0.count = 2 32 | } 33 | 34 | await store.send(.timer(.wrapped(.incrementButtonTapped))) { 35 | $0.count = 3 36 | } 37 | 38 | await store.send(.timer(.wrapped(.decrementButtonTapped))) { 39 | $0.count = 2 40 | } 41 | 42 | await store.send(.toggleTimerButtonTapped) { 43 | $0.count = nil 44 | } 45 | 46 | await store.send(.timer(.onDisappear)) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/Tests/NewGameCoreTests/NewGameCoreTests.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import GameCore 3 | import NewGameCore 4 | import XCTest 5 | 6 | @MainActor 7 | final class NewGameCoreTests: XCTestCase { 8 | let store = TestStore( 9 | initialState: NewGame.State(), 10 | reducer: NewGame() 11 | ) 12 | 13 | func testFlow_NewGame_Integration() async { 14 | await self.store.send(.oPlayerNameChanged("Blob Sr.")) { 15 | $0.oPlayerName = "Blob Sr." 16 | } 17 | await self.store.send(.xPlayerNameChanged("Blob Jr.")) { 18 | $0.xPlayerName = "Blob Jr." 19 | } 20 | await self.store.send(.letsPlayButtonTapped) { 21 | $0.game = Game.State(oPlayerName: "Blob Sr.", xPlayerName: "Blob Jr.") 22 | } 23 | await self.store.send(.game(.cellTapped(row: 0, column: 0))) { 24 | $0.game!.board[0][0] = .x 25 | $0.game!.currentPlayer = .o 26 | } 27 | await self.store.send(.game(.quitButtonTapped)) { 28 | $0.game = nil 29 | } 30 | await self.store.send(.letsPlayButtonTapped) { 31 | $0.game = Game.State(oPlayerName: "Blob Sr.", xPlayerName: "Blob Jr.") 32 | } 33 | await self.store.send(.gameDismissed) { 34 | $0.game = nil 35 | } 36 | await self.store.send(.logoutButtonTapped) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Examples/VoiceMemos/VoiceMemos/Dependencies.swift: -------------------------------------------------------------------------------- 1 | import Dependencies 2 | import SwiftUI 3 | import XCTestDynamicOverlay 4 | 5 | extension DependencyValues { 6 | var openSettings: @Sendable () async -> Void { 7 | get { self[OpenSettingsKey.self] } 8 | set { self[OpenSettingsKey.self] = newValue } 9 | } 10 | 11 | private enum OpenSettingsKey: DependencyKey { 12 | typealias Value = @Sendable () async -> Void 13 | 14 | static let liveValue: @Sendable () async -> Void = { 15 | await MainActor.run { 16 | UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!) 17 | } 18 | } 19 | static let testValue: @Sendable () async -> Void = unimplemented( 20 | #"@Dependency(\.openSettings)"# 21 | ) 22 | } 23 | 24 | var temporaryDirectory: @Sendable () -> URL { 25 | get { self[TemporaryDirectoryKey.self] } 26 | set { self[TemporaryDirectoryKey.self] = newValue } 27 | } 28 | 29 | private enum TemporaryDirectoryKey: DependencyKey { 30 | static let liveValue: @Sendable () -> URL = { URL(fileURLWithPath: NSTemporaryDirectory()) } 31 | static let testValue: @Sendable () -> URL = XCTUnimplemented( 32 | #"@Dependency(\.temporaryDirectory)"#, 33 | placeholder: URL(fileURLWithPath: NSTemporaryDirectory()) 34 | ) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/TestStore.md: -------------------------------------------------------------------------------- 1 | # ``ComposableArchitecture/TestStore`` 2 | 3 | ## Topics 4 | 5 | ### Creating a test store 6 | 7 | - ``init(initialState:reducer:prepareDependencies:file:line:)-55zkv`` 8 | - ``init(initialState:reducer:observe:prepareDependencies:file:line:)`` 9 | - ``init(initialState:reducer:observe:send:prepareDependencies:file:line:)`` 10 | 11 | ### Configuring a test store 12 | 13 | - ``dependencies`` 14 | - ``exhaustivity`` 15 | - ``timeout`` 16 | 17 | ### Testing a reducer 18 | 19 | - ``send(_:assert:file:line:)-1ax61`` 20 | - ``receive(_:timeout:assert:file:line:)-1rwdd`` 21 | - ``receive(_:timeout:assert:file:line:)-8xkqt`` 22 | - ``receive(_:timeout:assert:file:line:)-2ju31`` 23 | - ``finish(timeout:file:line:)`` 24 | - ``TestStoreTask`` 25 | 26 | ### Methods for skipping actions and effects 27 | 28 | - ``skipReceivedActions(strict:file:line:)-a4ri`` 29 | - ``skipInFlightEffects(strict:file:line:)-5hbsk`` 30 | 31 | ### Accessing state 32 | 33 | While the most common way of interacting with a test store's state is via its ``send(_:assert:file:line:)-1ax61`` and ``receive(_:timeout:assert:file:line:)-1rwdd`` methods, you may also access it directly throughout a test. 34 | 35 | - ``state`` 36 | 37 | ### Deprecations 38 | 39 | - 40 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Documentation.docc/Extensions/Deprecations/TestStoreDeprecations.md: -------------------------------------------------------------------------------- 1 | # Deprecations 2 | 3 | Review unsupported test store APIs and their replacements. 4 | 5 | ## Overview 6 | 7 | Avoid using deprecated APIs in your app. Select a method to see the replacement that you should use instead. 8 | 9 | ## Topics 10 | 11 | ### Creating a test store 12 | 13 | - ``TestStore/init(initialState:reducer:environment:file:line:)`` 14 | - ``TestStore/init(initialState:reducer:prepareDependencies:file:line:)-72tkt`` 15 | 16 | ### Configuring a test store 17 | 18 | - ``TestStore/environment`` 19 | 20 | ### Testing reducers 21 | 22 | - ``TestStore/send(_:assert:file:line:)-30pjj`` 23 | - ``TestStore/receive(_:assert:file:line:)-2nhm0`` 24 | - ``TestStore/receive(_:assert:file:line:)-1bfw4`` 25 | - ``TestStore/receive(_:assert:file:line:)-5o4u3`` 26 | - ``TestStore/assert(_:file:line:)-707lb`` 27 | - ``TestStore/assert(_:file:line:)-4gff7`` 28 | - ``TestStore/LocalState`` 29 | - ``TestStore/LocalAction`` 30 | - ``TestStore/Step`` 31 | 32 | ### Methods for skipping tests 33 | 34 | - ``TestStore/skipReceivedActions(strict:file:line:)-3nldt`` 35 | - ``TestStore/skipInFlightEffects(strict:file:line:)-95n5f`` 36 | 37 | ### Scoping test stores 38 | 39 | - ``TestStore/scope(state:action:)`` 40 | - ``TestStore/scope(state:)`` 41 | -------------------------------------------------------------------------------- /Sources/swift-composable-architecture-benchmark/Dependencies.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | import Combine 3 | import ComposableArchitecture 4 | import Dependencies 5 | import Foundation 6 | 7 | let dependenciesSuite = BenchmarkSuite(name: "Dependencies") { suite in 8 | #if swift(>=5.7) 9 | let reducer: some ReducerProtocol = BenchmarkReducer() 10 | .dependency(\.calendar, .autoupdatingCurrent) 11 | .dependency(\.date, .init { Date() }) 12 | .dependency(\.locale, .autoupdatingCurrent) 13 | .dependency(\.mainQueue, .immediate) 14 | .dependency(\.mainRunLoop, .immediate) 15 | .dependency(\.timeZone, .autoupdatingCurrent) 16 | .dependency(\.uuid, .init { UUID() }) 17 | 18 | suite.benchmark("Dependency key writing") { 19 | var state = 0 20 | _ = reducer.reduce(into: &state, action: ()) 21 | precondition(state == 1) 22 | } 23 | #endif 24 | } 25 | 26 | private struct BenchmarkReducer: ReducerProtocol { 27 | @Dependency(\.someValue) var someValue 28 | func reduce(into state: inout Int, action: Void) -> EffectTask { 29 | state = self.someValue 30 | return .none 31 | } 32 | } 33 | private enum SomeValueKey: DependencyKey { 34 | static let liveValue = 1 35 | } 36 | extension DependencyValues { 37 | var someValue: Int { 38 | self[SomeValueKey.self] 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Examples/Todos/Todos/Todo.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct Todo: ReducerProtocol { 5 | struct State: Equatable, Identifiable { 6 | var description = "" 7 | let id: UUID 8 | var isComplete = false 9 | } 10 | 11 | enum Action: Equatable { 12 | case checkBoxToggled 13 | case textFieldChanged(String) 14 | } 15 | 16 | func reduce(into state: inout State, action: Action) -> EffectTask { 17 | switch action { 18 | case .checkBoxToggled: 19 | state.isComplete.toggle() 20 | return .none 21 | 22 | case let .textFieldChanged(description): 23 | state.description = description 24 | return .none 25 | } 26 | } 27 | } 28 | 29 | struct TodoView: View { 30 | let store: StoreOf 31 | 32 | var body: some View { 33 | WithViewStore(self.store, observe: { $0 }) { viewStore in 34 | HStack { 35 | Button(action: { viewStore.send(.checkBoxToggled) }) { 36 | Image(systemName: viewStore.isComplete ? "checkmark.square" : "square") 37 | } 38 | .buttonStyle(.plain) 39 | 40 | TextField( 41 | "Untitled Todo", 42 | text: viewStore.binding(get: \.description, send: Todo.Action.textFieldChanged) 43 | ) 44 | } 45 | .foregroundColor(viewStore.isComplete ? .gray : nil) 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Examples/Integration/Integration/EscapedWithViewStoreTestCase.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | struct EscapedWithViewStoreTestCase: ReducerProtocol { 5 | enum Action: Equatable, Sendable { 6 | case incr 7 | case decr 8 | } 9 | 10 | func reduce(into state: inout Int, action: Action) -> EffectTask { 11 | switch action { 12 | case .incr: 13 | state += 1 14 | return .none 15 | case .decr: 16 | state -= 1 17 | return .none 18 | } 19 | } 20 | } 21 | 22 | struct EscapedWithViewStoreTestCaseView: View { 23 | let store: StoreOf 24 | 25 | var body: some View { 26 | VStack { 27 | WithViewStore(store, observe: { $0 }) { viewStore in 28 | GeometryReader { proxy in 29 | Text("\(viewStore.state)") 30 | .accessibilityValue("\(viewStore.state)") 31 | .accessibilityLabel("EscapedLabel") 32 | } 33 | Button("Button", action: { viewStore.send(.incr) }) 34 | Text("\(viewStore.state)") 35 | .accessibilityValue("\(viewStore.state)") 36 | .accessibilityLabel("Label") 37 | Stepper { 38 | Text("Stepper") 39 | } onIncrement: { 40 | viewStore.send(.incr) 41 | } onDecrement: { 42 | viewStore.send(.decr) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/Sources/GameCore/Three.swift: -------------------------------------------------------------------------------- 1 | /// A collection of three elements. 2 | public struct Three { 3 | public var first: Element 4 | public var second: Element 5 | public var third: Element 6 | 7 | public init(_ first: Element, _ second: Element, _ third: Element) { 8 | self.first = first 9 | self.second = second 10 | self.third = third 11 | } 12 | 13 | public func map(_ transform: (Element) -> T) -> Three { 14 | .init(transform(self.first), transform(self.second), transform(self.third)) 15 | } 16 | } 17 | 18 | extension Three: MutableCollection { 19 | public subscript(offset: Int) -> Element { 20 | _read { 21 | switch offset { 22 | case 0: yield self.first 23 | case 1: yield self.second 24 | case 2: yield self.third 25 | default: fatalError() 26 | } 27 | } 28 | _modify { 29 | switch offset { 30 | case 0: yield &self.first 31 | case 1: yield &self.second 32 | case 2: yield &self.third 33 | default: fatalError() 34 | } 35 | } 36 | } 37 | 38 | public var startIndex: Int { 0 } 39 | public var endIndex: Int { 3 } 40 | public func index(after i: Int) -> Int { i + 1 } 41 | } 42 | 43 | extension Three: RandomAccessCollection {} 44 | 45 | extension Three: Equatable where Element: Equatable {} 46 | extension Three: Hashable where Element: Hashable {} 47 | -------------------------------------------------------------------------------- /Sources/swift-composable-architecture-benchmark/Common.swift: -------------------------------------------------------------------------------- 1 | import Benchmark 2 | 3 | extension BenchmarkSuite { 4 | func benchmark( 5 | _ name: String, 6 | run: @escaping () throws -> Void, 7 | setUp: @escaping () -> Void = {}, 8 | tearDown: @escaping () -> Void 9 | ) { 10 | self.register( 11 | benchmark: Benchmarking(name: name, run: run, setUp: setUp, tearDown: tearDown) 12 | ) 13 | } 14 | } 15 | 16 | struct Benchmarking: AnyBenchmark { 17 | let name: String 18 | let settings: [BenchmarkSetting] = [] 19 | private let _run: () throws -> Void 20 | private let _setUp: () -> Void 21 | private let _tearDown: () -> Void 22 | 23 | init( 24 | name: String, 25 | run: @escaping () throws -> Void, 26 | setUp: @escaping () -> Void = {}, 27 | tearDown: @escaping () -> Void = {} 28 | ) { 29 | self.name = name 30 | self._run = run 31 | self._setUp = setUp 32 | self._tearDown = tearDown 33 | } 34 | 35 | func setUp() { 36 | self._setUp() 37 | } 38 | 39 | func run(_ state: inout BenchmarkState) throws { 40 | try self._run() 41 | } 42 | 43 | func tearDown() { 44 | self._tearDown() 45 | } 46 | } 47 | 48 | @inline(__always) 49 | func doNotOptimizeAway(_ x: T) { 50 | @_optimize(none) 51 | func assumePointeeIsRead(_ x: UnsafeRawPointer) {} 52 | 53 | withUnsafePointer(to: x) { assumePointeeIsRead($0) } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Reducer/Reducers/Reduce.swift: -------------------------------------------------------------------------------- 1 | /// A type-erased reducer that invokes the given `reduce` function. 2 | /// 3 | /// ``Reduce`` is useful for injecting logic into a reducer tree without the overhead of introducing 4 | /// a new type that conforms to ``ReducerProtocol``. 5 | public struct Reduce: ReducerProtocol { 6 | @usableFromInline 7 | let reduce: (inout State, Action) -> EffectTask 8 | 9 | @usableFromInline 10 | init( 11 | internal reduce: @escaping (inout State, Action) -> EffectTask 12 | ) { 13 | self.reduce = reduce 14 | } 15 | 16 | /// Initializes a reducer with a `reduce` function. 17 | /// 18 | /// - Parameter reduce: A function that is called when ``reduce(into:action:)`` is invoked. 19 | @inlinable 20 | public init(_ reduce: @escaping (inout State, Action) -> EffectTask) { 21 | self.init(internal: reduce) 22 | } 23 | 24 | /// Type-erases a reducer. 25 | /// 26 | /// - Parameter reducer: A reducer that is called when ``reduce(into:action:)`` is invoked. 27 | @inlinable 28 | public init(_ reducer: R) 29 | where R.State == State, R.Action == Action { 30 | self.init(internal: reducer.reduce) 31 | } 32 | 33 | @inlinable 34 | public func reduce(into state: inout State, action: Action) -> EffectTask { 35 | self.reduce(&state, action) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift: -------------------------------------------------------------------------------- 1 | import AuthenticationClient 2 | import ComposableArchitecture 3 | import Dispatch 4 | import LoginCore 5 | import NewGameCore 6 | 7 | public struct TicTacToe: ReducerProtocol { 8 | public enum State: Equatable { 9 | case login(Login.State) 10 | case newGame(NewGame.State) 11 | 12 | public init() { self = .login(Login.State()) } 13 | } 14 | 15 | public enum Action: Equatable { 16 | case login(Login.Action) 17 | case newGame(NewGame.Action) 18 | } 19 | 20 | public init() {} 21 | 22 | public var body: some ReducerProtocol { 23 | Reduce { state, action in 24 | switch action { 25 | case .login(.twoFactor(.twoFactorResponse(.success))): 26 | state = .newGame(NewGame.State()) 27 | return .none 28 | 29 | case let .login(.loginResponse(.success(response))) where !response.twoFactorRequired: 30 | state = .newGame(NewGame.State()) 31 | return .none 32 | 33 | case .login: 34 | return .none 35 | 36 | case .newGame(.logoutButtonTapped): 37 | state = .login(Login.State()) 38 | return .none 39 | 40 | case .newGame: 41 | return .none 42 | } 43 | } 44 | .ifCaseLet(/State.login, action: /Action.login) { 45 | Login() 46 | } 47 | .ifCaseLet(/State.newGame, action: /Action.newGame) { 48 | NewGame() 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Sources/ComposableArchitecture/Reducer/Reducers/CombineReducers.swift: -------------------------------------------------------------------------------- 1 | /// Combines multiple reducers into a single reducer. 2 | /// 3 | /// `CombineReducers` takes a block that can combine a number of reducers using a 4 | /// ``ReducerBuilder``. 5 | /// 6 | /// Useful for grouping reducers together and applying reducer modifiers to the result. 7 | /// 8 | /// ```swift 9 | /// var body: some ReducerProtocol { 10 | /// CombineReducers { 11 | /// ReducerA() 12 | /// ReducerB() 13 | /// ReducerC() 14 | /// } 15 | /// .ifLet(\.child, action: /Action.child) 16 | /// } 17 | /// ``` 18 | public struct CombineReducers: ReducerProtocol 19 | where State == Reducers.State, Action == Reducers.Action { 20 | @usableFromInline 21 | let reducers: Reducers 22 | 23 | /// Initializes a reducer that combines all of the reducers in the given build block. 24 | /// 25 | /// - Parameter build: A reducer builder. 26 | @inlinable 27 | public init( 28 | @ReducerBuilder _ build: () -> Reducers 29 | ) { 30 | self.init(internal: build()) 31 | } 32 | 33 | @usableFromInline 34 | init(internal reducers: Reducers) { 35 | self.reducers = reducers 36 | } 37 | 38 | @inlinable 39 | public func reduce( 40 | into state: inout Reducers.State, action: Reducers.Action 41 | ) -> EffectTask { 42 | self.reducers.reduce(into: &state, action: action) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Examples/Integration/IntegrationUITests/EscapedWithViewStoreTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | 3 | @MainActor 4 | final class EscapedWithViewStoreTests: XCTestCase { 5 | 6 | override func setUpWithError() throws { 7 | self.continueAfterFailure = false 8 | } 9 | 10 | func testExample() async throws { 11 | let app = XCUIApplication() 12 | app.launch() 13 | 14 | app.collectionViews.buttons["EscapedWithViewStoreTestCase"].tap() 15 | 16 | XCTAssertEqual(app.staticTexts["Label"].value as? String, "10") 17 | XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "10") 18 | 19 | app.buttons["Button"].tap() 20 | 21 | XCTAssertEqual(app.staticTexts["Label"].value as? String, "11") 22 | XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "11") 23 | 24 | let stepper = app.steppers["Stepper"] 25 | 26 | stepper.buttons["Increment"].tap() 27 | stepper.buttons["Increment"].tap() 28 | stepper.buttons["Increment"].tap() 29 | stepper.buttons["Increment"].tap() 30 | 31 | XCTAssertEqual(app.staticTexts["Label"].value as? String, "15") 32 | XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "15") 33 | 34 | stepper.buttons["Decrement"].tap() 35 | stepper.buttons["Decrement"].tap() 36 | stepper.buttons["Decrement"].tap() 37 | 38 | XCTAssertEqual(app.staticTexts["Label"].value as? String, "12") 39 | XCTAssertEqual(app.staticTexts["EscapedLabel"].value as? String, "12") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Tests/ComposableArchitectureTests/TaskCancellationTests.swift: -------------------------------------------------------------------------------- 1 | #if DEBUG 2 | import Combine 3 | import XCTest 4 | @_spi(Internals) import ComposableArchitecture 5 | 6 | final class TaskCancellationTests: XCTestCase { 7 | func testCancellation() async throws { 8 | _cancellablesLock.sync { 9 | _cancellationCancellables.removeAll() 10 | } 11 | enum ID {} 12 | let (stream, continuation) = AsyncStream.streamWithContinuation() 13 | let task = Task { 14 | try await withTaskCancellation(id: ID.self) { 15 | continuation.yield() 16 | continuation.finish() 17 | try await Task.never() 18 | } 19 | } 20 | await stream.first(where: { true }) 21 | Task.cancel(id: ID.self) 22 | await Task.megaYield(count: 20) 23 | XCTAssertEqual(_cancellablesLock.sync { _cancellationCancellables }, [:]) 24 | do { 25 | try await task.cancellableValue 26 | XCTFail() 27 | } catch { 28 | } 29 | } 30 | 31 | func testWithTaskCancellationCleansUpTask() async throws { 32 | let task = Task { 33 | try await withTaskCancellation(id: 0) { 34 | try await Task.sleep(nanoseconds: NSEC_PER_SEC * 1000) 35 | } 36 | } 37 | 38 | try await Task.sleep(nanoseconds: NSEC_PER_SEC / 3) 39 | XCTAssertEqual(_cancellationCancellables.count, 1) 40 | 41 | task.cancel() 42 | try await Task.sleep(nanoseconds: NSEC_PER_SEC / 3) 43 | XCTAssertEqual(_cancellationCancellables.count, 0) 44 | } 45 | } 46 | #endif 47 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Internal/TemplateText.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | 3 | extension Text { 4 | init(template: String, _ style: Font.TextStyle = .body) { 5 | enum Style: Hashable { 6 | case code 7 | case emphasis 8 | case strong 9 | } 10 | 11 | var segments: [Text] = [] 12 | var currentValue = "" 13 | var currentStyles: Set