├── .github ├── CODE_OF_CONDUCT.md └── workflows │ ├── ci.yml │ ├── documentation.yml │ └── format.yml ├── .gitignore ├── .spi.yml ├── ComposableArchitecture.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ ├── IDEWorkspaceChecks.plist │ ├── WorkspaceSettings.xcsettings │ ├── swiftpm │ └── Package.resolved │ └── xcschemes │ ├── ComposableArchitecture.xcscheme │ └── swift-composable-architecture-benchmark.xcscheme ├── Examples ├── .swiftpm │ └── xcode │ │ └── package.xcworkspace │ │ └── contents.xcworkspacedata ├── CaseStudies │ ├── CaseStudies.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ ├── CaseStudies (SwiftUI).xcscheme │ │ │ ├── CaseStudies (UIKit).xcscheme │ │ │ └── tvOSCaseStudies.xcscheme │ ├── README.md │ ├── SwiftUICaseStudies │ │ ├── 00-Core.swift │ │ ├── 00-RootView.swift │ │ ├── 01-GettingStarted-AlertsAndConfirmationDialogs.swift │ │ ├── 01-GettingStarted-Animations.swift │ │ ├── 01-GettingStarted-Bindings-Basics.swift │ │ ├── 01-GettingStarted-Bindings-Forms.swift │ │ ├── 01-GettingStarted-Composition-TwoCounters.swift │ │ ├── 01-GettingStarted-Counter.swift │ │ ├── 01-GettingStarted-FocusState.swift │ │ ├── 01-GettingStarted-OptionalState.swift │ │ ├── 01-GettingStarted-SharedState.swift │ │ ├── 02-Effects-Basics.swift │ │ ├── 02-Effects-Cancellation.swift │ │ ├── 02-Effects-LongLiving.swift │ │ ├── 02-Effects-Refreshable.swift │ │ ├── 02-Effects-SystemEnvironment.swift │ │ ├── 02-Effects-Timers.swift │ │ ├── 02-Effects-WebSocket.swift │ │ ├── 03-Navigation-Lists-LoadThenNavigate.swift │ │ ├── 03-Navigation-Lists-NavigateAndLoad.swift │ │ ├── 03-Navigation-LoadThenNavigate.swift │ │ ├── 03-Navigation-NavigateAndLoad.swift │ │ ├── 03-Navigation-Sheet-LoadThenPresent.swift │ │ ├── 03-Navigation-Sheet-PresentAndLoad.swift │ │ ├── 04-HigherOrderReducers-ElmLikeSubscriptions.swift │ │ ├── 04-HigherOrderReducers-Lifecycle.swift │ │ ├── 04-HigherOrderReducers-Recursion.swift │ │ ├── 04-HigherOrderReducers-ResuableOfflineDownloads │ │ │ ├── DownloadClient.swift │ │ │ ├── DownloadComponent.swift │ │ │ └── ReusableComponents-Download.swift │ │ ├── 04-HigherOrderReducers-ReusableFavoriting.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ ├── AppIcon.png │ │ │ │ ├── Contents.json │ │ │ │ └── transparent.png │ │ │ └── Contents.json │ │ ├── CaseStudiesApp.swift │ │ ├── FactClient.swift │ │ ├── Info.plist │ │ └── Internal │ │ │ ├── AboutView.swift │ │ │ ├── CircularProgressView.swift │ │ │ ├── ResignFirstResponder.swift │ │ │ ├── TemplateText.swift │ │ │ └── UIViewRepresented.swift │ ├── SwiftUICaseStudiesTests │ │ ├── 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift │ │ ├── 01-GettingStarted-AnimationsTests.swift │ │ ├── 01-GettingStarted-BindingBasicsTests.swift │ │ ├── 01-GettingStarted-SharedStateTests.swift │ │ ├── 02-Effects-BasicsTests.swift │ │ ├── 02-Effects-CancellationTests.swift │ │ ├── 02-Effects-LongLivingTests.swift │ │ ├── 02-Effects-RefreshableTests.swift │ │ ├── 02-Effects-TimersTests.swift │ │ ├── 02-Effects-WebSocketTests.swift │ │ ├── 04-HigherOrderReducers-LifecycleTests.swift │ │ ├── 04-HigherOrderReducers-RecursionTests.swift │ │ ├── 04-HigherOrderReducers-ReusableFavoritingTests.swift │ │ └── 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift │ ├── UIKitCaseStudies │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ ├── AppIcon.png │ │ │ │ ├── Contents.json │ │ │ │ └── transparent.png │ │ │ └── Contents.json │ │ ├── Base.lproj │ │ │ └── LaunchScreen.storyboard │ │ ├── CounterViewController.swift │ │ ├── Info.plist │ │ ├── Internal │ │ │ ├── ActivityIndicatorViewController.swift │ │ │ ├── IfLetStoreController.swift │ │ │ └── UIViewRepresented.swift │ │ ├── ListsOfState.swift │ │ ├── LoadThenNavigate.swift │ │ ├── NavigateAndLoad.swift │ │ ├── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ │ └── Contents.json │ │ ├── RootViewController.swift │ │ └── SceneDelegate.swift │ ├── UIKitCaseStudiesTests │ │ ├── Info.plist │ │ └── UIKitCaseStudiesTests.swift │ ├── tvOSCaseStudies │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon.png │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Core.swift │ │ ├── FocusView.swift │ │ ├── Info.plist │ │ └── RootView.swift │ └── tvOSCaseStudiesTests │ │ └── FocusTests.swift ├── Integration │ ├── Integration.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── swiftpm │ │ │ │ └── Package.resolved │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Integration.xcscheme │ ├── Integration │ │ ├── Assets.xcassets │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── BindingsAnimationsTestBench.swift │ │ ├── EscapedWithViewStoreTestCase.swift │ │ ├── ForEachBindingTestCase.swift │ │ ├── IntegrationApp.swift │ │ ├── NavigationStackBindingTestCase.swift │ │ └── Preview Content │ │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ └── IntegrationUITests │ │ ├── EscapedWithViewStoreTests.swift │ │ ├── ForEachBindingTests.swift │ │ └── NavigationStackBindingTests.swift ├── Package.swift ├── README.md ├── Search │ ├── README.md │ ├── Search.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Search.xcscheme │ ├── Search │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ ├── AppIcon.png │ │ │ │ ├── Contents.json │ │ │ │ └── transparent.png │ │ │ └── Contents.json │ │ ├── SearchApp.swift │ │ ├── SearchView.swift │ │ └── WeatherClient.swift │ └── SearchTests │ │ └── SearchTests.swift ├── SpeechRecognition │ ├── README.md │ ├── SpeechRecognition.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── SpeechRecognition.xcscheme │ ├── SpeechRecognition │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ ├── AppIcon.png │ │ │ │ ├── Contents.json │ │ │ │ └── transparent.png │ │ │ └── Contents.json │ │ ├── Info.plist │ │ ├── SpeechClient │ │ │ ├── Client.swift │ │ │ ├── Live.swift │ │ │ └── Models.swift │ │ ├── SpeechRecognition.swift │ │ └── SpeechRecognitionApp.swift │ └── SpeechRecognitionTests │ │ └── SpeechRecognitionTests.swift ├── TicTacToe │ ├── App │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ ├── AppIcon.png │ │ │ │ ├── Contents.json │ │ │ │ └── transparent.png │ │ │ └── Contents.json │ │ ├── RootView.swift │ │ └── TicTacToeApp.swift │ ├── README.md │ ├── TicTacToe.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── TicTacToe.xcscheme │ └── tic-tac-toe │ │ ├── .gitignore │ │ ├── Package.swift │ │ ├── Sources │ │ ├── AppCore │ │ │ └── AppCore.swift │ │ ├── AppSwiftUI │ │ │ └── AppView.swift │ │ ├── AppUIKit │ │ │ └── AppViewController.swift │ │ ├── AuthenticationClient │ │ │ └── AuthenticationClient.swift │ │ ├── AuthenticationClientLive │ │ │ └── LiveAuthenticationClient.swift │ │ ├── GameCore │ │ │ ├── GameCore.swift │ │ │ └── Three.swift │ │ ├── GameSwiftUI │ │ │ └── GameView.swift │ │ ├── GameUIKit │ │ │ └── GameViewController.swift │ │ ├── LoginCore │ │ │ └── LoginCore.swift │ │ ├── LoginSwiftUI │ │ │ └── LoginView.swift │ │ ├── LoginUIKit │ │ │ └── LoginViewController.swift │ │ ├── NewGameCore │ │ │ └── NewGameCore.swift │ │ ├── NewGameSwiftUI │ │ │ └── NewGameView.swift │ │ ├── NewGameUIKit │ │ │ └── NewGameViewController.swift │ │ ├── TwoFactorCore │ │ │ └── TwoFactorCore.swift │ │ ├── TwoFactorSwiftUI │ │ │ └── TwoFactorView.swift │ │ └── TwoFactorUIKit │ │ │ └── TwoFactorViewController.swift │ │ └── Tests │ │ ├── AppCoreTests │ │ └── AppCoreTests.swift │ │ ├── GameCoreTests │ │ └── GameCoreTests.swift │ │ ├── GameSwiftUITests │ │ └── GameSwiftUITests.swift │ │ ├── LoginCoreTests │ │ └── LoginCoreTests.swift │ │ ├── LoginSwiftUITests │ │ └── LoginSwiftUITests.swift │ │ ├── NewGameCoreTests │ │ └── NewGameCoreTests.swift │ │ ├── NewGameSwiftUITests │ │ └── NewGameSwiftUITests.swift │ │ ├── TwoFactorCoreTests │ │ └── TwoFactorCoreTests.swift │ │ └── TwoFactorSwiftUITests │ │ └── TwoFactorSwiftUITests.swift ├── Todos │ ├── README.md │ ├── Todos.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Todos.xcscheme │ ├── Todos │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── AppIcon-60@2x.png │ │ │ │ ├── AppIcon-76@2x.png │ │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ │ ├── AppIcon.png │ │ │ │ ├── Contents.json │ │ │ │ └── transparent.png │ │ │ └── Contents.json │ │ ├── Todo.swift │ │ ├── Todos.swift │ │ └── TodosApp.swift │ └── TodosTests │ │ └── TodosTests.swift └── VoiceMemos │ ├── README.md │ ├── VoiceMemos.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── VoiceMemos.xcscheme │ ├── VoiceMemos │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── AppIcon-60@2x.png │ │ │ ├── AppIcon-76@2x.png │ │ │ ├── AppIcon-iPadPro@2x.png │ │ │ ├── AppIcon.png │ │ │ ├── Contents.json │ │ │ └── transparent.png │ │ └── Contents.json │ ├── AudioPlayerClient │ │ ├── AudioPlayerClient.swift │ │ └── LiveAudioPlayerClient.swift │ ├── AudioRecorderClient │ │ ├── AudioRecorderClient.swift │ │ └── LiveAudioRecorderClient.swift │ ├── Dependencies.swift │ ├── Helpers.swift │ ├── Info.plist │ ├── RecordingMemo.swift │ ├── VoiceMemo.swift │ ├── VoiceMemos.swift │ └── VoiceMemosApp.swift │ └── VoiceMemosTests │ └── VoiceMemosTests.swift ├── LICENSE ├── Makefile ├── Package.resolved ├── Package.swift ├── README.md ├── Sources ├── ComposableArchitecture │ ├── Dependencies │ │ └── MainQueue.swift │ ├── Documentation.docc │ │ ├── Articles │ │ │ ├── Bindings.md │ │ │ ├── DependencyManagement.md │ │ │ ├── GettingStarted.md │ │ │ ├── MigratingToTheReducerProtocol.md │ │ │ ├── Performance.md │ │ │ ├── SwiftConcurrency.md │ │ │ └── Testing.md │ │ ├── ComposableArchitecture.md │ │ └── Extensions │ │ │ ├── AnyReducerDeprecations.md │ │ │ ├── Deprecations │ │ │ ├── EffectDeprecations.md │ │ │ ├── ReducerDeprecations.md │ │ │ ├── StoreDeprecations.md │ │ │ ├── SwiftUIDeprecations.md │ │ │ ├── TestStoreDeprecations.md │ │ │ └── ViewStoreDeprecations.md │ │ │ ├── Effect.md │ │ │ ├── EffectCancel.md │ │ │ ├── EffectCancelIds.md │ │ │ ├── EffectCancellable.md │ │ │ ├── EffectRun.md │ │ │ ├── EffectSend.md │ │ │ ├── ReducerBuilder.md │ │ │ ├── ReducerProtocol.md │ │ │ ├── Store.md │ │ │ ├── StoreScope.md │ │ │ ├── SwiftUI.md │ │ │ ├── SwitchStore.md │ │ │ ├── TestStore.md │ │ │ ├── TestStoreExhaustivity.md │ │ │ ├── UIKit.md │ │ │ ├── ViewStore.md │ │ │ ├── ViewStoreBinding.md │ │ │ ├── WithTaskCancellation.md │ │ │ ├── WithViewStore.md │ │ │ └── WithViewStoreInit.md │ ├── Effect.swift │ ├── Effects │ │ ├── Animation.swift │ │ ├── Cancellation.swift │ │ ├── Publisher │ │ │ ├── Debouncing.swift │ │ │ ├── Deferring.swift │ │ │ ├── Throttling.swift │ │ │ └── Timer.swift │ │ ├── SignalProducer.swift │ │ └── TaskResult.swift │ ├── Internal │ │ ├── Binding+IsPresent.swift │ │ ├── Box.swift │ │ ├── Breakpoint.swift │ │ ├── CurrentValueRelay.swift │ │ ├── Debug.swift │ │ ├── Deprecations.swift │ │ ├── Exports.swift │ │ ├── Locking.swift │ │ ├── OpenExistential.swift │ │ ├── RuntimeWarnings.swift │ │ ├── TaskCancellableValue.swift │ │ └── TypeName.swift │ ├── Reducer │ │ ├── AnyReducer │ │ │ ├── AnyReducer.swift │ │ │ ├── AnyReducerBinding.swift │ │ │ ├── AnyReducerCompatibility.swift │ │ │ ├── AnyReducerDebug.swift │ │ │ └── AnyReducerSignpost.swift │ │ ├── ReducerBuilder.swift │ │ └── Reducers │ │ │ ├── BindingReducer.swift │ │ │ ├── CombineReducers.swift │ │ │ ├── DebugReducer.swift │ │ │ ├── DependencyKeyWritingReducer.swift │ │ │ ├── EmptyReducer.swift │ │ │ ├── ForEachReducer.swift │ │ │ ├── IfCaseLetReducer.swift │ │ │ ├── IfLetReducer.swift │ │ │ ├── Optional.swift │ │ │ ├── Reduce.swift │ │ │ ├── Scope.swift │ │ │ └── SignpostReducer.swift │ ├── ReducerProtocol.swift │ ├── SchedulerExtensions │ │ └── SchedulerExtensions.swift │ ├── Store.swift │ ├── SwiftUI │ │ ├── ActionWrappingScheduler.swift │ │ ├── Alert.swift │ │ ├── Binding.swift │ │ ├── ConfirmationDialog.swift │ │ ├── ForEachStore.swift │ │ ├── Identified.swift │ │ ├── IfLetStore.swift │ │ ├── SwitchStore.swift │ │ └── WithViewStore.swift │ ├── TestStore.swift │ ├── TestSupport │ │ └── ImmediateScheduler.swift │ ├── UIKit │ │ ├── AlertStateUIKit.swift │ │ ├── IfLetUIKit.swift │ │ └── UIKitAnimationScheduler.swift │ └── ViewStore.swift ├── _CAsyncSupport │ ├── _CAsyncSupport.h │ └── module.modulemap └── swift-composable-architecture-benchmark │ ├── Common.swift │ ├── Dependencies.swift │ ├── Effects.swift │ ├── StoreScope.swift │ ├── ViewStore.swift │ └── main.swift └── Tests ├── ComposableArchitectureTests ├── BindingTests.swift ├── CompatibilityTests.swift ├── ComposableArchitectureTests.swift ├── DebugTests.swift ├── DependencyKeyWritingReducerTests.swift ├── DeprecatedTests.swift ├── EffectCancellationTests.swift ├── EffectDebounceTests.swift ├── EffectDeferredTests.swift ├── EffectFailureTests.swift ├── EffectOperationTests.swift ├── EffectRunTests.swift ├── EffectTaskTests.swift ├── EffectTests.swift ├── EffectThrottleTests.swift ├── ForEachReducerTests.swift ├── IfCaseLetReducerTests.swift ├── IfLetReducerTests.swift ├── MemoryManagementTests.swift ├── ReducerBuilderTests.swift ├── ReducerTests.swift ├── RuntimeWarningTests.swift ├── SchedulerTests.swift ├── ScopeTests.swift ├── SerialExecutor.swift ├── StoreTests.swift ├── TaskCancellationTests.swift ├── TaskResultTests.swift ├── TestStoreFailureTests.swift ├── TestStoreNonExhaustiveTests.swift ├── TestStoreTests.swift ├── TimerTests.swift ├── ViewStoreTests.swift └── WithViewStoreAppTest.swift └── LinuxMain.swift /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | pull_request: 9 | branches: 10 | - '*' 11 | workflow_dispatch: 12 | 13 | concurrency: 14 | group: ci-${{ github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | library: 19 | runs-on: macos-12 20 | strategy: 21 | matrix: 22 | xcode: 23 | - '14.1' 24 | config: ['debug', 'release'] 25 | steps: 26 | - uses: actions/checkout@v3 27 | - name: Select Xcode ${{ matrix.xcode }} 28 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 29 | - name: Run ${{ matrix.config }} tests 30 | run: CONFIG=${{ matrix.config }} make test-library 31 | 32 | library-evolution: 33 | runs-on: macos-12 34 | strategy: 35 | matrix: 36 | xcode: 37 | - '14.1' 38 | steps: 39 | - uses: actions/checkout@v3 40 | - name: Select Xcode ${{ matrix.xcode }} 41 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 42 | - name: Build for library evolution 43 | run: make build-for-library-evolution 44 | 45 | benchmarks: 46 | runs-on: macos-12 47 | strategy: 48 | matrix: 49 | xcode: 50 | - '14.1' 51 | steps: 52 | - uses: actions/checkout@v3 53 | - name: Select Xcode ${{ matrix.xcode }} 54 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 55 | - name: Run benchmark 56 | run: make benchmark 57 | 58 | examples: 59 | runs-on: macos-12 60 | strategy: 61 | matrix: 62 | xcode: 63 | - '14.1' 64 | steps: 65 | - uses: actions/checkout@v3 66 | - name: Select Xcode ${{ matrix.xcode }} 67 | run: sudo xcode-select -s /Applications/Xcode_${{ matrix.xcode }}.app 68 | - name: Run tests 69 | run: make test-examples 70 | 71 | swiftpm-linux: 72 | name: SwiftPM Linux 73 | runs-on: ubuntu-latest 74 | steps: 75 | - uses: actions/checkout@v2 76 | - name: Swift version 77 | run: swift --version 78 | - name: Test via SwiftPM 79 | run: swift test --enable-test-discovery 80 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Documentation 2 | on: 3 | release: 4 | types: 5 | - published 6 | - protocol-clocks 7 | workflow_dispatch: 8 | 9 | concurrency: 10 | group: docs-${{ github.ref }} 11 | cancel-in-progress: true 12 | 13 | jobs: 14 | build: 15 | runs-on: macos-12 16 | steps: 17 | - name: Select Xcode 14.1 18 | run: sudo xcode-select -s /Applications/Xcode_14.1.app 19 | 20 | - name: Checkout Package 21 | uses: actions/checkout@v2 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Checkout gh-pages Branch 26 | uses: actions/checkout@v2 27 | with: 28 | ref: gh-pages 29 | path: docs-out 30 | 31 | - name: Build documentation 32 | run: > 33 | rm -rf docs-out/.git; 34 | rm -rf docs-out/master; 35 | git tag -l --sort=-v:refname | grep -e "\d\+\.\d\+.0" | tail -n +6 | xargs -I {} rm -rf {}; 36 | 37 | for tag in $(echo "master"; git tag -l --sort=-v:refname | grep -e "\d\+\.\d\+.0" | head -6); 38 | do 39 | if [ -d "docs-out/$tag/data/documentation/composablearchitecture" ] 40 | then 41 | echo "✅ Documentation for "$tag" already exists."; 42 | else 43 | echo "⏳ Generating documentation for ComposableArchitecture @ "$tag" release."; 44 | rm -rf "docs-out/$tag"; 45 | 46 | git checkout .; 47 | git checkout "$tag"; 48 | 49 | swift package \ 50 | --allow-writing-to-directory docs-out/"$tag" \ 51 | generate-documentation \ 52 | --target ComposableArchitecture \ 53 | --output-path docs-out/"$tag" \ 54 | --transform-for-static-hosting \ 55 | --hosting-base-path /reactiveswift-composable-architecture/"$tag" \ 56 | && echo "✅ Documentation generated for ComposableArchitecture @ "$tag" release." \ 57 | || echo "⚠️ Documentation skipped for ComposableArchitecture @ "$tag"."; 58 | fi; 59 | done 60 | 61 | - name: Fix permissions 62 | run: 'sudo chown -R $USER docs-out' 63 | 64 | - name: Publish documentation to GitHub Pages 65 | uses: JamesIves/github-pages-deploy-action@4.1.7 66 | with: 67 | branch: gh-pages 68 | folder: docs-out 69 | single-commit: true 70 | -------------------------------------------------------------------------------- /.github/workflows/format.yml: -------------------------------------------------------------------------------- 1 | name: Format 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 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@v3 18 | - name: Xcode Select 19 | run: sudo xcode-select -s /Applications/Xcode_14.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: 'master' 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /.swiftpm 4 | /Packages 5 | /*.xcodeproj 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /ComposableArchitecture.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 12 | 13 | 15 | 16 | 18 | 19 | 21 | 22 | 24 | 25 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ComposableArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Examples/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Composition-TwoCounters.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | private let readMe = """ 5 | This screen demonstrates how to take small features and compose them into bigger ones using reducer builders and the `Scope` reducer, as well as the `scope` operator on stores. 6 | 7 | It reuses the domain of the counter screen and embeds it, twice, in a larger domain. 8 | """ 9 | 10 | // MARK: - Feature domain 11 | 12 | struct TwoCounters: ReducerProtocol { 13 | struct State: Equatable { 14 | var counter1 = Counter.State() 15 | var counter2 = Counter.State() 16 | } 17 | 18 | enum Action: Equatable { 19 | case counter1(Counter.Action) 20 | case counter2(Counter.Action) 21 | } 22 | 23 | var body: some ReducerProtocol { 24 | Scope(state: \.counter1, action: /Action.counter1) { 25 | Counter() 26 | } 27 | Scope(state: \.counter2, action: /Action.counter2) { 28 | Counter() 29 | } 30 | } 31 | } 32 | 33 | // MARK: - Feature view 34 | 35 | struct TwoCountersView: View { 36 | let store: StoreOf 37 | 38 | var body: some View { 39 | Form { 40 | Section { 41 | AboutView(readMe: readMe) 42 | } 43 | 44 | HStack { 45 | Text("Counter 1") 46 | Spacer() 47 | CounterView( 48 | store: self.store.scope(state: \.counter1, action: TwoCounters.Action.counter1) 49 | ) 50 | } 51 | 52 | HStack { 53 | Text("Counter 2") 54 | Spacer() 55 | CounterView( 56 | store: self.store.scope(state: \.counter2, action: TwoCounters.Action.counter2) 57 | ) 58 | } 59 | } 60 | .buttonStyle(.borderless) 61 | .navigationTitle("Two counters demo") 62 | } 63 | } 64 | 65 | // MARK: - SwiftUI previews 66 | 67 | struct TwoCountersView_Previews: PreviewProvider { 68 | static var previews: some View { 69 | NavigationView { 70 | TwoCountersView( 71 | store: Store( 72 | initialState: TwoCounters.State(), 73 | reducer: TwoCounters() 74 | ) 75 | ) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Counter.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import SwiftUI 3 | 4 | private let readMe = """ 5 | This screen demonstrates the basics of the Composable Architecture in an archetypal counter \ 6 | application. 7 | 8 | The domain of the application is modeled using simple data types that correspond to the mutable \ 9 | state of the application and any actions that can affect that state or the outside world. 10 | """ 11 | 12 | // MARK: - Feature domain 13 | 14 | struct Counter: ReducerProtocol { 15 | struct State: Equatable { 16 | var count = 0 17 | } 18 | 19 | enum Action: Equatable { 20 | case decrementButtonTapped 21 | case incrementButtonTapped 22 | } 23 | 24 | func reduce(into state: inout State, action: Action) -> EffectTask { 25 | switch action { 26 | case .decrementButtonTapped: 27 | state.count -= 1 28 | return .none 29 | case .incrementButtonTapped: 30 | state.count += 1 31 | return .none 32 | } 33 | } 34 | } 35 | 36 | // MARK: - Feature view 37 | 38 | struct CounterView: View { 39 | let store: StoreOf 40 | 41 | var body: some View { 42 | WithViewStore(self.store, observe: { $0 }) { viewStore in 43 | HStack { 44 | Button { 45 | viewStore.send(.decrementButtonTapped) 46 | } label: { 47 | Image(systemName: "minus") 48 | } 49 | 50 | Text("\(viewStore.count)") 51 | .monospacedDigit() 52 | 53 | Button { 54 | viewStore.send(.incrementButtonTapped) 55 | } label: { 56 | Image(systemName: "plus") 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | struct CounterDemoView: View { 64 | let store: StoreOf 65 | 66 | var body: some View { 67 | Form { 68 | Section { 69 | AboutView(readMe: readMe) 70 | } 71 | 72 | Section { 73 | CounterView(store: self.store) 74 | .frame(maxWidth: .infinity) 75 | } 76 | } 77 | .buttonStyle(.borderless) 78 | .navigationTitle("Counter demo") 79 | } 80 | } 81 | 82 | // MARK: - SwiftUI previews 83 | 84 | struct CounterView_Previews: PreviewProvider { 85 | static var previews: some View { 86 | NavigationView { 87 | CounterDemoView( 88 | store: Store( 89 | initialState: Counter.State(), 90 | reducer: Counter() 91 | ) 92 | ) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import Foundation 3 | import XCTestDynamicOverlay 4 | 5 | struct DownloadClient { 6 | var download: @Sendable (URL) -> AsyncThrowingStream 7 | 8 | enum Event: Equatable { 9 | case response(Data) 10 | case updateProgress(Double) 11 | } 12 | } 13 | 14 | extension DependencyValues { 15 | var downloadClient: DownloadClient { 16 | get { self[DownloadClient.self] } 17 | set { self[DownloadClient.self] = newValue } 18 | } 19 | } 20 | 21 | extension DownloadClient: DependencyKey { 22 | static let liveValue = Self( 23 | download: { url in 24 | .init { continuation in 25 | Task { 26 | do { 27 | let (bytes, response) = try await URLSession.shared.bytes(from: url) 28 | var data = Data() 29 | var progress = 0 30 | for try await byte in bytes { 31 | data.append(byte) 32 | let newProgress = Int( 33 | Double(data.count) / Double(response.expectedContentLength) * 100) 34 | if newProgress != progress { 35 | progress = newProgress 36 | continuation.yield(.updateProgress(Double(progress) / 100)) 37 | } 38 | } 39 | continuation.yield(.response(data)) 40 | continuation.finish() 41 | } catch { 42 | continuation.finish(throwing: error) 43 | } 44 | } 45 | } 46 | } 47 | ) 48 | 49 | static let testValue = Self( 50 | download: unimplemented("\(Self.self).download") 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/reactiveswift-composable-architecture/c3f39b7c1a0ba15912c1b9c5c9e8ed2447f87467/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/ReactiveCocoa/reactiveswift-composable-architecture/c3f39b7c1a0ba15912c1b9c5c9e8ed2447f87467/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/reactiveswift-composable-architecture/c3f39b7c1a0ba15912c1b9c5c9e8ed2447f87467/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/reactiveswift-composable-architecture/c3f39b7c1a0ba15912c1b9c5c9e8ed2447f87467/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "filename" : "AppIcon-60@2x.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "60x60" 38 | }, 39 | { 40 | "filename" : "AppIcon.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "60x60" 44 | }, 45 | { 46 | "idiom" : "ipad", 47 | "scale" : "1x", 48 | "size" : "20x20" 49 | }, 50 | { 51 | "idiom" : "ipad", 52 | "scale" : "2x", 53 | "size" : "20x20" 54 | }, 55 | { 56 | "idiom" : "ipad", 57 | "scale" : "1x", 58 | "size" : "29x29" 59 | }, 60 | { 61 | "idiom" : "ipad", 62 | "scale" : "2x", 63 | "size" : "29x29" 64 | }, 65 | { 66 | "idiom" : "ipad", 67 | "scale" : "1x", 68 | "size" : "40x40" 69 | }, 70 | { 71 | "idiom" : "ipad", 72 | "scale" : "2x", 73 | "size" : "40x40" 74 | }, 75 | { 76 | "idiom" : "ipad", 77 | "scale" : "1x", 78 | "size" : "76x76" 79 | }, 80 | { 81 | "filename" : "AppIcon-76@2x.png", 82 | "idiom" : "ipad", 83 | "scale" : "2x", 84 | "size" : "76x76" 85 | }, 86 | { 87 | "filename" : "AppIcon-iPadPro@2x.png", 88 | "idiom" : "ipad", 89 | "scale" : "2x", 90 | "size" : "83.5x83.5" 91 | }, 92 | { 93 | "filename" : "transparent.png", 94 | "idiom" : "ios-marketing", 95 | "scale" : "1x", 96 | "size" : "1024x1024" 97 | } 98 | ], 99 | "info" : { 100 | "author" : "xcode", 101 | "version" : 1 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ReactiveCocoa/reactiveswift-composable-architecture/c3f39b7c1a0ba15912c1b9c5c9e8ed2447f87467/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/FactClient.swift: -------------------------------------------------------------------------------- 1 | import ComposableArchitecture 2 | import Foundation 3 | import ReactiveSwift 4 | import XCTestDynamicOverlay 5 | 6 | struct FactClient { 7 | var fetch: @Sendable (Int) async throws -> String 8 | } 9 | 10 | extension DependencyValues { 11 | var factClient: FactClient { 12 | get { self[FactClient.self] } 13 | set { self[FactClient.self] = newValue } 14 | } 15 | } 16 | 17 | extension FactClient: DependencyKey { 18 | /// This is the "live" fact dependency that reaches into the outside world to fetch trivia. 19 | /// Typically this live implementation of the dependency would live in its own module so that the 20 | /// main feature doesn't need to compile it. 21 | static let liveValue = Self( 22 | fetch: { number in 23 | try await Task.sleep(nanoseconds: NSEC_PER_SEC) 24 | let (data, _) = try await URLSession.shared 25 | .data(from: URL(string: "http://numbersapi.com/\(number)/trivia")!) 26 | return String(decoding: data, as: UTF8.self) 27 | } 28 | ) 29 | 30 | /// This is the "unimplemented" fact dependency that is useful to plug into tests that you want 31 | /// to prove do not need the dependency. 32 | static let testValue = Self( 33 | fetch: unimplemented("\(Self.self).fetch") 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/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 | NSAppTransportSecurity 26 | 27 | NSAllowsArbitraryLoads 28 | 29 | 30 | UIRequiredDeviceCapabilities 31 | 32 | armv7 33 | 34 | UISupportedInterfaceOrientations 35 | 36 | UIInterfaceOrientationPortrait 37 | UIInterfaceOrientationLandscapeLeft 38 | UIInterfaceOrientationLandscapeRight 39 | 40 | UISupportedInterfaceOrientations~ipad 41 | 42 | UIInterfaceOrientationPortrait 43 | UIInterfaceOrientationPortraitUpsideDown 44 | UIInterfaceOrientationLandscapeLeft 45 | UIInterfaceOrientationLandscapeRight 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftUI) 2 | import SwiftUI 3 | 4 | extension Binding { 5 | /// SwiftUI will print errors to the console about "AttributeGraph: cycle detected" if you disable 6 | /// a text field while it is focused. This hack will force all fields to unfocus before we write 7 | /// to a binding that may disable the fields. 8 | /// 9 | /// See also: https://stackoverflow.com/a/69653555 10 | @MainActor 11 | func resignFirstResponder() -> Self { 12 | Self( 13 | get: { self.wrappedValue }, 14 | set: { newValue, transaction in 15 | UIApplication.shared.sendAction( 16 | #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil 17 | ) 18 | self.transaction(transaction).wrappedValue = newValue 19 | } 20 | ) 21 | } 22 | } 23 | #endif 24 | -------------------------------------------------------------------------------- /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