├── .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