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