From e652eb7df4f70aa1c0091fc5922fb536cbd483c4 Mon Sep 17 00:00:00 2001 From: Hadi Dbouk Date: Thu, 21 Nov 2024 21:20:37 +0200 Subject: [PATCH] Initial commit --- .DS_Store | Bin 0 -> 8196 bytes .build/workspace-state.json | 178 +++ .gitattributes | 2 + .github/CODE_OF_CONDUCT.md | 84 ++ .github/ISSUE_TEMPLATE/bug_report.yml | 71 + .github/ISSUE_TEMPLATE/config.yml | 12 + .github/workflows/ci.yml | 38 + .github/workflows/documentation.yml | 105 ++ .github/workflows/format.yml | 27 + .spi.yml | 12 + .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 70210 bytes .../xcschemes/xcschememanagement.plist | 72 + .../contents.xcworkspacedata | 25 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcschemes/ComposableArchitecture.xcscheme | 78 + ...composable-architecture-benchmark.xcscheme | 88 ++ .../contents.xcworkspacedata | 7 + .../CaseStudies.xcodeproj/project.pbxproj | 1277 +++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/CaseStudies (SwiftUI).xcscheme | 88 ++ .../xcschemes/CaseStudies (UIKit).xcscheme | 88 ++ .../xcschemes/tvOSCaseStudies.xcscheme | 88 ++ Examples/CaseStudies/README.md | 3 + .../SwiftUICaseStudies/00-Core.swift | 283 ++++ .../SwiftUICaseStudies/00-RootView.swift | 304 ++++ ...Started-AlertsAndConfirmationDialogs.swift | 125 ++ .../01-GettingStarted-Animations.swift | 170 +++ .../01-GettingStarted-Bindings-Basics.swift | 138 ++ .../01-GettingStarted-Bindings-Forms.swift | 123 ++ ...ttingStarted-Composition-TwoCounters.swift | 79 + .../01-GettingStarted-Counter.swift | 90 ++ .../01-GettingStarted-FocusState.swift | 97 ++ .../01-GettingStarted-OptionalState.swift | 110 ++ .../01-GettingStarted-SharedState.swift | 278 ++++ .../02-Effects-Basics.swift | 177 +++ .../02-Effects-Cancellation.swift | 141 ++ .../02-Effects-LongLiving.swift | 110 ++ .../02-Effects-Refreshable.swift | 132 ++ .../02-Effects-SystemEnvironment.swift | 243 ++++ .../02-Effects-Timers.swift | 130 ++ .../02-Effects-WebSocket.swift | 341 +++++ ...03-Navigation-Lists-LoadThenNavigate.swift | 156 ++ .../03-Navigation-Lists-NavigateAndLoad.swift | 136 ++ .../03-Navigation-LoadThenNavigate.swift | 125 ++ .../03-Navigation-NavigateAndLoad.swift | 115 ++ .../03-Navigation-Sheet-LoadThenPresent.swift | 126 ++ .../03-Navigation-Sheet-PresentAndLoad.swift | 114 ++ ...erOrderReducers-ElmLikeSubscriptions.swift | 150 ++ .../04-HigherOrderReducers-Lifecycle.swift | 174 +++ .../04-HigherOrderReducers-Recursion.swift | 170 +++ .../DownloadClient.swift | 46 + .../DownloadComponent.swift | 184 +++ .../ReusableComponents-Download.swift | 293 ++++ ...gherOrderReducers-ReusableFavoriting.swift | 225 +++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../Assets.xcassets/Contents.json | 6 + .../SwiftUICaseStudies/CaseStudiesApp.swift | 20 + .../SwiftUICaseStudies/FactClient.swift | 32 + .../CaseStudies/SwiftUICaseStudies/Info.plist | 48 + .../Internal/AboutView.swift | 11 + .../Internal/CircularProgressView.swift | 23 + .../Internal/ResignFirstResponder.swift | 21 + .../Internal/TemplateText.swift | 59 + .../Internal/UIViewRepresented.swift | 14 + ...ed-AlertsAndConfirmationDialogsTests.swift | 60 + .../01-GettingStarted-AnimationsTests.swift | 101 ++ ...01-GettingStarted-BindingBasicsTests.swift | 33 + .../01-GettingStarted-SharedStateTests.swift | 111 ++ .../02-Effects-BasicsTests.swift | 87 ++ .../02-Effects-CancellationTests.swift | 103 ++ .../02-Effects-LongLivingTests.swift | 35 + .../02-Effects-RefreshableTests.swift | 72 + .../02-Effects-TimersTests.swift | 46 + .../02-Effects-WebSocketTests.swift | 177 +++ ...4-HigherOrderReducers-LifecycleTests.swift | 50 + ...rderReducers-ReusableFavoritingTests.swift | 79 + ...ducers-ReusableOfflineDownloadsTests.swift | 162 +++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + .../CounterViewController.swift | 97 ++ .../CaseStudies/UIKitCaseStudies/Info.plist | 60 + .../ActivityIndicatorViewController.swift | 21 + .../Internal/IfLetStoreController.swift | 52 + .../Internal/UIViewRepresented.swift | 14 + .../UIKitCaseStudies/ListsOfState.swift | 99 ++ .../UIKitCaseStudies/LoadThenNavigate.swift | 157 ++ .../UIKitCaseStudies/NavigateAndLoad.swift | 142 ++ .../Preview Assets.xcassets/Contents.json | 6 + .../UIKitCaseStudies/RootViewController.swift | 101 ++ .../UIKitCaseStudies/SceneDelegate.swift | 27 + .../UIKitCaseStudiesTests/Info.plist | 22 + .../UIKitCaseStudiesTests.swift | 57 + .../tvOSCaseStudies/AppDelegate.swift | 27 + .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8152 bytes .../AppIcon.appiconset/Contents.json | 99 ++ .../Assets.xcassets/Contents.json | 6 + .../CaseStudies/tvOSCaseStudies/Core.swift | 21 + .../tvOSCaseStudies/FocusView.swift | 100 ++ .../CaseStudies/tvOSCaseStudies/Info.plist | 32 + .../tvOSCaseStudies/RootView.swift | 49 + .../tvOSCaseStudiesTests.swift | 6 + Examples/Package.swift | 9 + Examples/README.md | 21 + Examples/Search/README.md | 13 + .../Search/Search.xcodeproj/project.pbxproj | 504 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Search.xcscheme | 88 ++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../Search/Assets.xcassets/Contents.json | 6 + Examples/Search/Search/SearchApp.swift | 21 + Examples/Search/Search/SearchView.swift | 238 +++ Examples/Search/Search/WeatherClient.swift | 156 ++ Examples/Search/SearchTests/SearchTests.swift | 216 +++ Examples/SpeechRecognition/README.md | 5 + .../project.pbxproj | 506 +++++++ .../xcschemes/SpeechRecognition.xcscheme | 88 ++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../Assets.xcassets/Contents.json | 6 + .../SpeechRecognition/Info.plist | 64 + .../SpeechClient/Client.swift | 17 + .../SpeechRecognition/SpeechClient/Live.swift | 100 ++ .../SpeechClient/Models.swift | 94 ++ .../SpeechClient/Unimplemented.swift | 18 + .../SpeechRecognition/SpeechRecognition.swift | 207 +++ .../SpeechRecognitionApp.swift | 21 + .../SpeechRecognitionTests.swift | 145 ++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../App/Assets.xcassets/Contents.json | 6 + Examples/TicTacToe/App/RootView.swift | 70 + Examples/TicTacToe/App/TicTacToeApp.swift | 10 + Examples/TicTacToe/README.md | 21 + .../TicTacToe.xcodeproj/project.pbxproj | 358 +++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/TicTacToe.xcscheme | 168 +++ Examples/TicTacToe/tic-tac-toe/.gitignore | 7 + Examples/TicTacToe/tic-tac-toe/Package.swift | 191 +++ .../tic-tac-toe/Sources/AppCore/AppCore.swift | 62 + .../Sources/AppSwiftUI/AppView.swift | 30 + .../Sources/AppUIKit/AppViewController.swift | 58 + .../AuthenticationClient.swift | 81 ++ .../LiveAuthenticationClient.swift | 28 + .../Sources/GameCore/GameCore.swift | 114 ++ .../tic-tac-toe/Sources/GameCore/Three.swift | 46 + .../Sources/GameSwiftUI/GameView.swift | 107 ++ .../GameUIKit/GameViewController.swift | 171 +++ .../Sources/LoginCore/LoginCore.swift | 99 ++ .../Sources/LoginSwiftUI/LoginView.swift | 140 ++ .../LoginUIKit/LoginViewController.swift | 212 +++ .../Sources/NewGameCore/NewGameCore.swift | 66 + .../Sources/NewGameSwiftUI/NewGameView.swift | 112 ++ .../NewGameUIKit/NewGameViewController.swift | 182 +++ .../Sources/TwoFactorCore/TwoFactorCore.swift | 70 + .../TwoFactorSwiftUI/TwoFactorView.swift | 108 ++ .../TwoFactorViewController.swift | 132 ++ .../Tests/AppCoreTests/AppCoreTests.swift | 128 ++ .../Tests/GameCoreTests/GameCoreTests.swift | 80 ++ .../GameSwiftUITests/GameSwiftUITests.swift | 88 ++ .../Tests/LoginCoreTests/LoginCoreTests.swift | 110 ++ .../LoginSwiftUITests/LoginSwiftUITests.swift | 122 ++ .../NewGameCoreTests/NewGameCoreTests.swift | 39 + .../NewGameSwiftUITests.swift | 32 + .../TwoFactorCoreTests.swift | 75 + .../TwoFactorSwiftUITests.swift | 92 ++ Examples/Todos/README.md | 8 + .../Todos/Todos.xcodeproj/project.pbxproj | 504 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Todos.xcscheme | 88 ++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../Todos/Todos/Assets.xcassets/Contents.json | 6 + Examples/Todos/Todos/Todo.swift | 49 + Examples/Todos/Todos/Todos.swift | 204 +++ Examples/Todos/Todos/TodosApp.swift | 22 + Examples/Todos/TodosTests/TodosTests.swift | 353 +++++ Examples/VoiceMemos/README.md | 9 + .../VoiceMemos.xcodeproj/project.pbxproj | 538 +++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/VoiceMemos.xcscheme | 88 ++ .../AppIcon.appiconset/AppIcon-60@2x.png | Bin 0 -> 7569 bytes .../AppIcon.appiconset/AppIcon-76@2x.png | Bin 0 -> 9893 bytes .../AppIcon.appiconset/AppIcon-iPadPro@2x.png | Bin 0 -> 15226 bytes .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8460 bytes .../AppIcon.appiconset/Contents.json | 103 ++ .../AppIcon.appiconset/transparent.png | Bin 0 -> 221 bytes .../VoiceMemos/Assets.xcassets/Contents.json | 6 + .../AudioPlayerClient/AudioPlayerClient.swift | 6 + .../LiveAudioPlayerClient.swift | 54 + .../UnimplementedAudioPlayerClient.swift | 11 + .../AudioRecorderClient.swift | 9 + .../LiveAudioRecorderClient.swift | 107 ++ .../UnimplementedAudioRecorderClient.swift | 16 + Examples/VoiceMemos/VoiceMemos/Helpers.swift | 8 + Examples/VoiceMemos/VoiceMemos/Info.plist | 62 + .../VoiceMemos/VoiceMemos/RecordingMemo.swift | 121 ++ .../VoiceMemos/VoiceMemos/VoiceMemo.swift | 138 ++ .../VoiceMemos/VoiceMemos/VoiceMemos.swift | 290 ++++ .../VoiceMemos/VoiceMemos/VoiceMemosApp.swift | 31 + .../VoiceMemosTests/VoiceMemosTests.swift | 312 ++++ LICENSE | 21 + LocalPackages/.DS_Store | Bin 0 -> 12292 bytes Makefile | 75 + Package.swift | 56 + README.md | 501 +++++++ .../Debugging/ReducerDebugging.swift | 177 +++ .../Debugging/ReducerInstrumentation.swift | 147 ++ .../GettingReadyForSwiftConcurrency.md | 97 ++ .../Articles/GettingStarted.md | 261 ++++ .../Articles/Performance.md | 350 +++++ .../Documentation.docc/Articles/SwiftUI.md | 44 + .../Articles/SwiftUIDeprecations.md | 36 + .../Documentation.docc/Articles/Testing.md | 561 ++++++++ .../Documentation.docc/Articles/UIKit.md | 17 + .../ComposableArchitecture.md | 77 + .../Documentation.docc/Extensions/Effect.md | 44 + .../Extensions/EffectDeprecations.md | 50 + .../Extensions/EffectRun.md | 7 + .../Documentation.docc/Extensions/Reducer.md | 42 + .../Extensions/ReducerDeprecations.md | 21 + .../Documentation.docc/Extensions/Store.md | 26 + .../Extensions/StoreDeprecations.md | 15 + .../Extensions/SwitchStore.md | 8 + .../Extensions/TestStore.md | 34 + .../Extensions/TestStoreDeprecations.md | 19 + .../Extensions/ViewStore.md | 36 + .../Extensions/ViewStoreDeprecations.md | 18 + .../ComposableArchitectureOld/Effect.swift | 591 ++++++++ .../Effects/Animation.swift | 82 ++ .../Effects/Cancellation.swift | 302 ++++ .../Effects/ConcurrencySupport.swift | 371 +++++ .../Effects/Debouncing.swift | 89 ++ .../Effects/Deferring.swift | 44 + .../Effects/Publisher.swift | 494 +++++++ .../Effects/TaskResult.swift | 305 ++++ .../Effects/Throttling.swift | 94 ++ .../Effects/Timer.swift | 149 ++ .../Internal/Binding+IsPresent.swift | 13 + .../Internal/Box.swift | 12 + .../Internal/Create.swift | 193 +++ .../Internal/CurrentValueRelay.swift | 55 + .../Internal/Debug.swift | 9 + .../Internal/Deprecations.swift | 1128 +++++++++++++++ .../Internal/Exports.swift | 4 + .../Internal/Locking.swift | 19 + .../Internal/RuntimeWarnings.swift | 52 + .../Internal/TaskCancellableValue.swift | 24 + .../ComposableArchitectureOld/Reducer.swift | 829 +++++++++++ Sources/ComposableArchitectureOld/Store.swift | 602 ++++++++ .../SwiftUI/Alert.swift | 414 ++++++ .../SwiftUI/Binding.swift | 602 ++++++++ .../SwiftUI/ConfirmationDialog.swift | 315 ++++ .../SwiftUI/ForEachStore.swift | 136 ++ .../SwiftUI/Identified.swift | 60 + .../SwiftUI/IfLetStore.swift | 105 ++ .../SwiftUI/SwitchStore.swift | 1260 ++++++++++++++++ .../SwiftUI/TextState.swift | 503 +++++++ .../SwiftUI/WithViewStore.swift | 660 +++++++++ .../ComposableArchitectureOld/TestStore.swift | 1064 ++++++++++++++ .../UIKit/AlertStateUIKit.swift | 108 ++ .../UIKit/IfLetUIKit.swift | 67 + .../ComposableArchitectureOld/ViewStore.swift | 549 +++++++ .../Common.swift | 54 + .../Effects.swift | 28 + .../StoreScope.swift | 25 + .../main.swift | 8 + .../BindingTests.swift | 37 + .../CompatibilityTests.swift | 125 ++ .../ComposableArchitectureTests.swift | 161 +++ .../DebugTests.swift | 203 +++ .../DeprecatedTests.swift | 34 + .../EffectCancellationTests.swift | 346 +++++ .../EffectDebounceTests.swift | 89 ++ .../EffectDeferredTests.swift | 83 ++ .../EffectFailureTests.swift | 54 + .../EffectOperationTests.swift | 150 ++ .../EffectRunTests.swift | 118 ++ .../EffectTaskTests.swift | 118 ++ .../EffectTests.swift | 255 ++++ .../EffectThrottleTests.swift | 218 +++ Tests/ComposableArchitectureTests/LCRNG.swift | 15 + .../MemoryManagementTests.swift | 40 + .../ReducerTests.swift | 227 +++ .../RuntimeWarningTests.swift | 220 +++ .../StoreTests.swift | 525 +++++++ .../TaskCancellationTests.swift | 27 + .../TaskResultTests.swift | 119 ++ .../TestStoreFailureTests.swift | 273 ++++ .../TestStoreTests.swift | 218 +++ .../TimerTests.swift | 123 ++ .../ViewStoreTests.swift | 315 ++++ .../WithViewStoreAppTest.swift | 69 + 327 files changed, 38142 insertions(+) create mode 100644 .DS_Store create mode 100644 .build/workspace-state.json create mode 100644 .gitattributes create mode 100755 .github/CODE_OF_CONDUCT.md create mode 100755 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100755 .github/ISSUE_TEMPLATE/config.yml create mode 100755 .github/workflows/ci.yml create mode 100755 .github/workflows/documentation.yml create mode 100755 .github/workflows/format.yml create mode 100755 .spi.yml create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 .swiftpm/xcode/package.xcworkspace/xcuserdata/hadi.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 .swiftpm/xcode/xcuserdata/hadi.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100755 ComposableArchitecture.xcworkspace/contents.xcworkspacedata create mode 100755 ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 ComposableArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100755 ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme create mode 100755 ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme create mode 100755 Examples/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100755 Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj create mode 100755 Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100755 Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (SwiftUI).xcscheme create mode 100755 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (UIKit).xcscheme create mode 100755 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/tvOSCaseStudies.xcscheme create mode 100755 Examples/CaseStudies/README.md create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Basics.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Composition-TwoCounters.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Counter.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-FocusState.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-OptionalState.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-LongLiving.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Refreshable.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-SystemEnvironment.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Timers.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-LoadThenNavigate.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-NavigateAndLoad.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-LoadThenNavigate.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-NavigateAndLoad.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-LoadThenPresent.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-PresentAndLoad.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Recursion.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/CaseStudiesApp.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/FactClient.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Info.plist create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Internal/AboutView.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Internal/CircularProgressView.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Internal/TemplateText.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudies/Internal/UIViewRepresented.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AlertsAndConfirmationDialogsTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-AnimationsTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-BindingBasicsTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/01-GettingStarted-SharedStateTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-BasicsTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-CancellationTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-LongLivingTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-RefreshableTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-TimersTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/02-Effects-WebSocketTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-LifecycleTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableFavoritingTests.swift create mode 100755 Examples/CaseStudies/SwiftUICaseStudiesTests/04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard create mode 100755 Examples/CaseStudies/UIKitCaseStudies/CounterViewController.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Info.plist create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Internal/UIViewRepresented.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/ListsOfState.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/LoadThenNavigate.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/NavigateAndLoad.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/Preview Content/Preview Assets.xcassets/Contents.json create mode 100755 Examples/CaseStudies/UIKitCaseStudies/RootViewController.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift create mode 100755 Examples/CaseStudies/UIKitCaseStudiesTests/Info.plist create mode 100755 Examples/CaseStudies/UIKitCaseStudiesTests/UIKitCaseStudiesTests.swift create mode 100755 Examples/CaseStudies/tvOSCaseStudies/AppDelegate.swift create mode 100755 Examples/CaseStudies/tvOSCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/CaseStudies/tvOSCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/CaseStudies/tvOSCaseStudies/Assets.xcassets/Contents.json create mode 100755 Examples/CaseStudies/tvOSCaseStudies/Core.swift create mode 100755 Examples/CaseStudies/tvOSCaseStudies/FocusView.swift create mode 100755 Examples/CaseStudies/tvOSCaseStudies/Info.plist create mode 100755 Examples/CaseStudies/tvOSCaseStudies/RootView.swift create mode 100755 Examples/CaseStudies/tvOSCaseStudiesTests/tvOSCaseStudiesTests.swift create mode 100755 Examples/Package.swift create mode 100755 Examples/README.md create mode 100755 Examples/Search/README.md create mode 100755 Examples/Search/Search.xcodeproj/project.pbxproj create mode 100755 Examples/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100755 Examples/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 Examples/Search/Search.xcodeproj/xcshareddata/xcschemes/Search.xcscheme create mode 100755 Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/Search/Search/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/Search/Search/Assets.xcassets/Contents.json create mode 100755 Examples/Search/Search/SearchApp.swift create mode 100755 Examples/Search/Search/SearchView.swift create mode 100755 Examples/Search/Search/WeatherClient.swift create mode 100755 Examples/Search/SearchTests/SearchTests.swift create mode 100755 Examples/SpeechRecognition/README.md create mode 100755 Examples/SpeechRecognition/SpeechRecognition.xcodeproj/project.pbxproj create mode 100755 Examples/SpeechRecognition/SpeechRecognition.xcodeproj/xcshareddata/xcschemes/SpeechRecognition.xcscheme create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Assets.xcassets/Contents.json create mode 100755 Examples/SpeechRecognition/SpeechRecognition/Info.plist create mode 100755 Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Client.swift create mode 100755 Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Live.swift create mode 100755 Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Models.swift create mode 100755 Examples/SpeechRecognition/SpeechRecognition/SpeechClient/Unimplemented.swift create mode 100755 Examples/SpeechRecognition/SpeechRecognition/SpeechRecognition.swift create mode 100755 Examples/SpeechRecognition/SpeechRecognition/SpeechRecognitionApp.swift create mode 100755 Examples/SpeechRecognition/SpeechRecognitionTests/SpeechRecognitionTests.swift create mode 100755 Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/TicTacToe/App/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/TicTacToe/App/Assets.xcassets/Contents.json create mode 100755 Examples/TicTacToe/App/RootView.swift create mode 100755 Examples/TicTacToe/App/TicTacToeApp.swift create mode 100755 Examples/TicTacToe/README.md create mode 100755 Examples/TicTacToe/TicTacToe.xcodeproj/project.pbxproj create mode 100755 Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100755 Examples/TicTacToe/TicTacToe.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 Examples/TicTacToe/TicTacToe.xcodeproj/xcshareddata/xcschemes/TicTacToe.xcscheme create mode 100755 Examples/TicTacToe/tic-tac-toe/.gitignore create mode 100755 Examples/TicTacToe/tic-tac-toe/Package.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/AppCore/AppCore.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/AppSwiftUI/AppView.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/AppUIKit/AppViewController.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClient/AuthenticationClient.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/AuthenticationClientLive/LiveAuthenticationClient.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/GameCore/GameCore.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/GameCore/Three.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/GameSwiftUI/GameView.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/GameUIKit/GameViewController.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/LoginCore/LoginCore.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/LoginSwiftUI/LoginView.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/LoginUIKit/LoginViewController.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/NewGameCore/NewGameCore.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/NewGameSwiftUI/NewGameView.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/NewGameUIKit/NewGameViewController.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorCore/TwoFactorCore.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorSwiftUI/TwoFactorView.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Sources/TwoFactorUIKit/TwoFactorViewController.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/AppCoreTests/AppCoreTests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/GameCoreTests/GameCoreTests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/GameSwiftUITests/GameSwiftUITests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/LoginCoreTests/LoginCoreTests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/LoginSwiftUITests/LoginSwiftUITests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/NewGameCoreTests/NewGameCoreTests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/NewGameSwiftUITests/NewGameSwiftUITests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorCoreTests/TwoFactorCoreTests.swift create mode 100755 Examples/TicTacToe/tic-tac-toe/Tests/TwoFactorSwiftUITests/TwoFactorSwiftUITests.swift create mode 100755 Examples/Todos/README.md create mode 100755 Examples/Todos/Todos.xcodeproj/project.pbxproj create mode 100755 Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100755 Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos.xcscheme create mode 100755 Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/Todos/Todos/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/Todos/Todos/Assets.xcassets/Contents.json create mode 100755 Examples/Todos/Todos/Todo.swift create mode 100755 Examples/Todos/Todos/Todos.swift create mode 100755 Examples/Todos/Todos/TodosApp.swift create mode 100755 Examples/Todos/TodosTests/TodosTests.swift create mode 100755 Examples/VoiceMemos/README.md create mode 100755 Examples/VoiceMemos/VoiceMemos.xcodeproj/project.pbxproj create mode 100755 Examples/VoiceMemos/VoiceMemos.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100755 Examples/VoiceMemos/VoiceMemos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100755 Examples/VoiceMemos/VoiceMemos.xcodeproj/xcshareddata/xcschemes/VoiceMemos.xcscheme create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/AppIcon.appiconset/transparent.png create mode 100755 Examples/VoiceMemos/VoiceMemos/Assets.xcassets/Contents.json create mode 100755 Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/AudioPlayerClient.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/LiveAudioPlayerClient.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/AudioPlayerClient/UnimplementedAudioPlayerClient.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/AudioRecorderClient.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/LiveAudioRecorderClient.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/AudioRecorderClient/UnimplementedAudioRecorderClient.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/Helpers.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/Info.plist create mode 100755 Examples/VoiceMemos/VoiceMemos/RecordingMemo.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/VoiceMemo.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/VoiceMemos.swift create mode 100755 Examples/VoiceMemos/VoiceMemos/VoiceMemosApp.swift create mode 100755 Examples/VoiceMemos/VoiceMemosTests/VoiceMemosTests.swift create mode 100755 LICENSE create mode 100644 LocalPackages/.DS_Store create mode 100755 Makefile create mode 100755 Package.swift create mode 100755 README.md create mode 100755 Sources/ComposableArchitectureOld/Debugging/ReducerDebugging.swift create mode 100755 Sources/ComposableArchitectureOld/Debugging/ReducerInstrumentation.swift create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/GettingReadyForSwiftConcurrency.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/GettingStarted.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/Performance.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/SwiftUI.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/SwiftUIDeprecations.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/Testing.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Articles/UIKit.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/ComposableArchitecture.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/Effect.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/EffectDeprecations.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/EffectRun.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/Reducer.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/ReducerDeprecations.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/Store.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/StoreDeprecations.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/SwitchStore.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/TestStore.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/TestStoreDeprecations.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/ViewStore.md create mode 100755 Sources/ComposableArchitectureOld/Documentation.docc/Extensions/ViewStoreDeprecations.md create mode 100755 Sources/ComposableArchitectureOld/Effect.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Animation.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Cancellation.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/ConcurrencySupport.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Debouncing.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Deferring.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Publisher.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/TaskResult.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Throttling.swift create mode 100755 Sources/ComposableArchitectureOld/Effects/Timer.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Binding+IsPresent.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Box.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Create.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/CurrentValueRelay.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Debug.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Deprecations.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Exports.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/Locking.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/RuntimeWarnings.swift create mode 100755 Sources/ComposableArchitectureOld/Internal/TaskCancellableValue.swift create mode 100755 Sources/ComposableArchitectureOld/Reducer.swift create mode 100755 Sources/ComposableArchitectureOld/Store.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/Alert.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/Binding.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/ConfirmationDialog.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/ForEachStore.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/Identified.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/IfLetStore.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/SwitchStore.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/TextState.swift create mode 100755 Sources/ComposableArchitectureOld/SwiftUI/WithViewStore.swift create mode 100755 Sources/ComposableArchitectureOld/TestStore.swift create mode 100755 Sources/ComposableArchitectureOld/UIKit/AlertStateUIKit.swift create mode 100755 Sources/ComposableArchitectureOld/UIKit/IfLetUIKit.swift create mode 100755 Sources/ComposableArchitectureOld/ViewStore.swift create mode 100755 Sources/swift-composable-architecture-benchmark-old/Common.swift create mode 100755 Sources/swift-composable-architecture-benchmark-old/Effects.swift create mode 100755 Sources/swift-composable-architecture-benchmark-old/StoreScope.swift create mode 100755 Sources/swift-composable-architecture-benchmark-old/main.swift create mode 100755 Tests/ComposableArchitectureTests/BindingTests.swift create mode 100755 Tests/ComposableArchitectureTests/CompatibilityTests.swift create mode 100755 Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift create mode 100755 Tests/ComposableArchitectureTests/DebugTests.swift create mode 100755 Tests/ComposableArchitectureTests/DeprecatedTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectCancellationTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectDebounceTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectDeferredTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectFailureTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectOperationTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectRunTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectTaskTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectTests.swift create mode 100755 Tests/ComposableArchitectureTests/EffectThrottleTests.swift create mode 100755 Tests/ComposableArchitectureTests/LCRNG.swift create mode 100755 Tests/ComposableArchitectureTests/MemoryManagementTests.swift create mode 100755 Tests/ComposableArchitectureTests/ReducerTests.swift create mode 100755 Tests/ComposableArchitectureTests/RuntimeWarningTests.swift create mode 100755 Tests/ComposableArchitectureTests/StoreTests.swift create mode 100755 Tests/ComposableArchitectureTests/TaskCancellationTests.swift create mode 100755 Tests/ComposableArchitectureTests/TaskResultTests.swift create mode 100755 Tests/ComposableArchitectureTests/TestStoreFailureTests.swift create mode 100755 Tests/ComposableArchitectureTests/TestStoreTests.swift create mode 100755 Tests/ComposableArchitectureTests/TimerTests.swift create mode 100755 Tests/ComposableArchitectureTests/ViewStoreTests.swift create mode 100755 Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..7c9675d69975051c0b2fceba0002ba85d7c8b91d GIT binary patch literal 8196 zcmeHM&rcgi6n+CFUPCEPfD~0~Yt@RB10pz)kg6PT3>HHD0m3LDN{F*-kDZm*JJ#+R zLlKhC)XkJs~b6L-ov3TYhB!fPYM~zKS)E!Sa=g?-tFkl!k3>XFs1BQWj zfdSmvvRD_~`}(PA4FiUO_mTk_AM&VDmSQ>3QiTpQDg}U?z_cppBM(qI`B;`>InYw6 zVojYruvnGFDF)N(c%GMV$Wkl^T55G-TAf(DGmA46rgn#3Ucrf#v^1?@z%X#00hzmB zr6qExM{X*Am#%alb?HA_hm9rj5|$AV5s zUF@+$N7NxNg+F~Qz7Kp8R&}W}iXUGC{zc*hpVJ!b=m06OkByUi#Bt7m>Nt};8AmU| z>0m2tIK-&0EN?5jY>d-q`1hPm73kS&7rA;kYSUONh0oAevIYWV%GPoV z$H#Y97W2iscTN`b$Hn3u3|78aIXTH%^M%{@c4}>}b8 zAH*kQ-w#jt=kSEt$*B*fKb*;3o4GzSJ3BXjMb5tO|if7}Y( zMzw3#UB9&#@x!RjQ>nZ3CZ#;c9wR_YBX71I5T5coY+*j*uHJc%i_7eR^PeL%f z8S&1h-xOH?0PJpcBUkw010IBs`=0G^Z_6)>C=#7Vb`aQpi zexYCK5BiJ#VT?_(SvJo;WuLKSR%9z|RjZB-f4z@K_fQx0CBNQ5HSj&*OV5!jSoAgC z`8IZM!e`-Qqdud&J{h;BQ=TGYOdK;tHB}5t`b?H~;FtGl9XnPK4>}{?I1%-jeD9JP z8ThbJ@dJ?}?fXo|iX{39?Mf6_Oh*an_ems%rWgj^7z5*arG+WT%EjOR-`IX;A;W-S z;N4?@O{`bf%dl&BHK$FKxpp4)8LBK2H_%e4pi${Kq)Nvj&;DVEejZS%Q;Ow4OU$6W T^$!91FJ*K8EA~zEzB2GX4* + rm -rf docs-out/.git; + rm -rf docs-out/main; + rm -rf docs-out/protocol; + + for tag in $(echo "main"; echo "protocol"; git tag); + do + echo "⏳ Generating documentation for "$tag" release."; + + if [ -d "docs-out/$tag" ] + then + echo "✅ Documentation for "$tag" already exists."; + else + git checkout "$tag"; + mkdir -p Sources/ComposableArchitecture/Documentation.docc; + export DOCC_HTML_DIR="$(pwd)/swift-docc-render/dist"; + + rm -rf .build/symbol-graphs; + mkdir -p .build/symbol-graphs; + swift build \ + --target ComposableArchitecture \ + -Xswiftc \ + -emit-symbol-graph \ + -Xswiftc \ + -emit-symbol-graph-dir \ + -Xswiftc \ + .build/symbol-graphs \ + && swift-docc/.build/release/docc convert Sources/ComposableArchitecture/Documentation.docc \ + --fallback-display-name ComposableArchitecture \ + --fallback-bundle-identifier co.pointfree.ComposableArchitecture \ + --fallback-bundle-version 0.0.0 \ + --additional-symbol-graph-dir \ + .build/symbol-graphs \ + --transform-for-static-hosting \ + --hosting-base-path /swift-composable-architecture/"$tag" \ + --output-path docs-out/"$tag" \ + && echo "✅ Documentation generated for "$tag" release." \ + || echo "⚠️ Documentation skipped for "$tag"."; + fi; + done + + - name: Fix permissions + run: 'sudo chown -R $USER docs-out' + - name: Publish documentation to GitHub Pages + uses: JamesIves/github-pages-deploy-action@4.1.7 + with: + branch: gh-pages + folder: docs-out + single-commit: true diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100755 index 0000000..500e270 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,27 @@ +name: Format + +on: + push: + branches: + - main + +jobs: + swift_format: + name: swift-format + runs-on: macos-12 + steps: + - uses: actions/checkout@v2 + - name: Xcode Select + run: sudo xcode-select -s /Applications/Xcode_13.4.1.app + - name: Tap + run: brew tap pointfreeco/formulae + - name: Install + run: brew install Formulae/swift-format@5.6 + - name: Format + run: make format + - uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Run swift-format + branch: 'main' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.spi.yml b/.spi.yml new file mode 100755 index 0000000..0a78973 --- /dev/null +++ b/.spi.yml @@ -0,0 +1,12 @@ +version: 1 +builder: + configs: + - platform: ios + scheme: ComposableArchitecture + - platform: macos-xcodebuild + scheme: ComposableArchitecture + - platform: tvos + scheme: ComposableArchitecture + - platform: watchos + scheme: ComposableArchitecture + - documentation_targets: [ComposableArchitecture] diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/hadi.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/hadi.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..c5b4e71a5b2f94985441dc2db96baceb48260a51 GIT binary patch literal 70210 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 literal 0 HcmV?d00001 diff --git a/.swiftpm/xcode/xcuserdata/hadi.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/hadi.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..7f0a4a9 --- /dev/null +++ b/.swiftpm/xcode/xcuserdata/hadi.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,72 @@ + + + + + SchemeUserState + + ComposableArchitecture.xcscheme_^#shared#^_ + + orderHint + 4 + + ComposableArchitectureOld.xcscheme_^#shared#^_ + + orderHint + 1 + + swift-composable-architecture-Package.xcscheme_^#shared#^_ + + orderHint + 3 + + swift-composable-architecture-benchmark-old.xcscheme_^#shared#^_ + + orderHint + 2 + + swift-composable-architecture-benchmark.xcscheme_^#shared#^_ + + orderHint + 2 + + swift-composable-architecture-old-Package.xcscheme_^#shared#^_ + + orderHint + 0 + + swift-composable-architecture-old.xcscheme_^#shared#^_ + + orderHint + 3 + + + SuppressBuildableAutocreation + + ComposableArchitecture + + primary + + + ComposableArchitectureOld + + primary + + + ComposableArchitectureTests + + primary + + + swift-composable-architecture-benchmark + + primary + + + swift-composable-architecture-benchmark-old + + primary + + + + + diff --git a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..5cc1612 --- /dev/null +++ b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ComposableArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100755 index 0000000..08de0be --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme b/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme new file mode 100755 index 0000000..98d7b89 --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/ComposableArchitecture.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme b/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme new file mode 100755 index 0000000..267ef6c --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/Examples/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..919434a --- /dev/null +++ b/Examples/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj new file mode 100755 index 0000000..13caf65 --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -0,0 +1,1277 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 52; + objects = { + +/* Begin PBXBuildFile section */ + 4F5AC11F24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */; }; + CA0C0C4724B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */; }; + CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */; }; + CA25E5D224463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */; }; + CA27C0B7245780CE00CB1E59 /* 02-Effects-SystemEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA27C0B6245780CE00CB1E59 /* 02-Effects-SystemEnvironment.swift */; }; + CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */; }; + CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */; }; + CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */; }; + CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */; }; + CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */; }; + CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */; }; + CA5ECF92267A79F0002067FF /* FactClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA5ECF91267A79F0002067FF /* FactClient.swift */; }; + CA6AC2642451135C00C71CB3 /* ReusableComponents-Download.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */; }; + CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */; }; + CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */; }; + CA6AC2672451135C00C71CB3 /* DownloadClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA6AC2632451135C00C71CB3 /* DownloadClient.swift */; }; + CA7BC8EE245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */; }; + CAA9ADC22446587C0003A984 /* 02-Effects-Basics.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */; }; + CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */; }; + CAA9ADC624465C810003A984 /* 02-Effects-Cancellation.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC524465C810003A984 /* 02-Effects-Cancellation.swift */; }; + CAA9ADC824465D950003A984 /* 02-Effects-CancellationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */; }; + CAA9ADCA2446605B0003A984 /* 02-Effects-LongLiving.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADC92446605B0003A984 /* 02-Effects-LongLiving.swift */; }; + CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */; }; + CABC4F3926AEE00C00D5FA2C /* 02-Effects-Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABC4F3826AEE00C00D5FA2C /* 02-Effects-Refreshable.swift */; }; + CABC4F3B26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CABC4F3A26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift */; }; + CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift */; }; + CAF069D024ACC5AF00A1AAEF /* 00-Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF069CF24ACC5AF00A1AAEF /* 00-Core.swift */; }; + CAF88E7324B8E26D00539345 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E7224B8E26D00539345 /* AppDelegate.swift */; }; + CAF88E7524B8E26D00539345 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E7424B8E26D00539345 /* RootView.swift */; }; + CAF88E7724B8E26E00539345 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CAF88E7624B8E26E00539345 /* Assets.xcassets */; }; + CAF88E8824B8E26E00539345 /* tvOSCaseStudiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E8724B8E26E00539345 /* tvOSCaseStudiesTests.swift */; }; + CAF88E9124B8E3AF00539345 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = CAF88E9024B8E3AF00539345 /* ComposableArchitecture */; }; + CAF88E9324B8E3D000539345 /* Core.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E9224B8E3D000539345 /* Core.swift */; }; + CAF88E9524B8E4D500539345 /* FocusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CAF88E9424B8E4D500539345 /* FocusView.swift */; }; + DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */; }; + DC072322244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC072321244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift */; }; + DC13940E2469E25C00EE1157 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC13940D2469E25C00EE1157 /* ComposableArchitecture */; }; + DC1394102469E27300EE1157 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = DC13940F2469E27300EE1157 /* ComposableArchitecture */; }; + DC25DC5F2450F13200082E81 /* IfLetStoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */; }; + DC25DC612450F2B000082E81 /* LoadThenNavigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC602450F2B000082E81 /* LoadThenNavigate.swift */; }; + DC25DC642450F2DF00082E81 /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC25DC632450F2DF00082E81 /* ActivityIndicatorViewController.swift */; }; + DC27215625BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */; }; + DC4C6EAC2450DD380066A05D /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6EAB2450DD380066A05D /* SceneDelegate.swift */; }; + DC4C6EAE2450DD380066A05D /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6EAD2450DD380066A05D /* RootViewController.swift */; }; + DC4C6EB02450DD380066A05D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC4C6EAF2450DD380066A05D /* Assets.xcassets */; }; + DC4C6EB32450DD380066A05D /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC4C6EB22450DD380066A05D /* Preview Assets.xcassets */; }; + DC4C6EB62450DD380066A05D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = DC4C6EB42450DD380066A05D /* LaunchScreen.storyboard */; }; + DC4C6EC12450DD390066A05D /* UIKitCaseStudiesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6EC02450DD390066A05D /* UIKitCaseStudiesTests.swift */; }; + DC4C6ED62450E1050066A05D /* CounterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6ED52450E1050066A05D /* CounterViewController.swift */; }; + DC4C6ED82450E4570066A05D /* UIViewRepresented.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6ED72450E4570066A05D /* UIViewRepresented.swift */; }; + DC4C6EDA2450E6050066A05D /* NavigateAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC4C6ED92450E6050066A05D /* NavigateAndLoad.swift */; }; + DC5B505125C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */; }; + DC630FDA2451016B00BAECBA /* ListsOfState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC630FD92451016B00BAECBA /* ListsOfState.swift */; }; + DC634B252448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */; }; + DC85EBC3285A731E00431CF3 /* ResignFirstResponder.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC85EBC2285A731E00431CF3 /* ResignFirstResponder.swift */; }; + DC88D8A6245341EC0077F427 /* 01-GettingStarted-Animations.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */; }; + DC89C41B24460F95006900B9 /* 00-RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C41A24460F95006900B9 /* 00-RootView.swift */; }; + DC89C41D24460F96006900B9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = DC89C41C24460F96006900B9 /* Assets.xcassets */; }; + DC89C4442446111B006900B9 /* 01-GettingStarted-Counter.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */; }; + DC89C449244618D5006900B9 /* 03-Navigation-LoadThenNavigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C448244618D5006900B9 /* 03-Navigation-LoadThenNavigate.swift */; }; + DC89C44D244621A5006900B9 /* 03-Navigation-NavigateAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C44C244621A5006900B9 /* 03-Navigation-NavigateAndLoad.swift */; }; + DC89C45124462DE7006900B9 /* 03-Navigation-Lists-LoadThenNavigate.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45024462DE7006900B9 /* 03-Navigation-Lists-LoadThenNavigate.swift */; }; + DC89C45324465452006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45224465451006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift */; }; + DC89C45524465C44006900B9 /* 02-Effects-Timers.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC89C45424465C44006900B9 /* 02-Effects-Timers.swift */; }; + DC9EB4172450CBD2005F413B /* UIViewRepresented.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9EB4162450CBD2005F413B /* UIViewRepresented.swift */; }; + DCAC2A4F2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCAC2A4E2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift */; }; + DCC68EAB244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EAA244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift */; }; + DCC68EDD2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */; }; + DCC68EDF2447BC810037F998 /* TemplateText.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EDE2447BC810037F998 /* TemplateText.swift */; }; + DCC68EE12447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */; }; + DCC68EE32447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCC68EE22447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift */; }; + DCD442C6286CA91F008B4EA7 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD442C5286CA91F008B4EA7 /* AboutView.swift */; }; + DCE63B71245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCE63B70245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift */; }; + DCFE1960278DBF0600C14CCF /* CaseStudiesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFE195F278DBF0600C14CCF /* CaseStudiesApp.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CAF88E8424B8E26E00539345 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC89C40B24460F95006900B9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = CAF88E6F24B8E26D00539345; + remoteInfo = tvOSCaseStudies; + }; + DC4C6EBD2450DD390066A05D /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC89C40B24460F95006900B9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DC4C6EA62450DD380066A05D; + remoteInfo = UIKitCaseStudies; + }; + DC89C42A24460F96006900B9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = DC89C40B24460F95006900B9 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DC89C41224460F95006900B9; + remoteInfo = CaseStudies; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + DC4C6ECD2450E0B30066A05D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6ED32450E0BA0066A05D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C43D2446106D006900B9 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C44124461077006900B9 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-SharedStateTests.swift"; sourceTree = ""; }; + CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-LifecycleTests.swift"; sourceTree = ""; }; + CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift"; sourceTree = ""; }; + CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Basics.swift"; sourceTree = ""; }; + CA27C0B6245780CE00CB1E59 /* 02-Effects-SystemEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-SystemEnvironment.swift"; sourceTree = ""; }; + CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AnimationsTests.swift"; sourceTree = ""; }; + CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-FocusState.swift"; sourceTree = ""; }; + CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-Lifecycle.swift"; sourceTree = ""; }; + CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocket.swift"; sourceTree = ""; }; + CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-WebSocketTests.swift"; sourceTree = ""; }; + CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndConfirmationDialogsTests.swift"; sourceTree = ""; }; + CA5ECF91267A79F0002067FF /* FactClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FactClient.swift; sourceTree = ""; }; + CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "ReusableComponents-Download.swift"; sourceTree = ""; }; + CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CircularProgressView.swift; sourceTree = ""; }; + CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadComponent.swift; sourceTree = ""; }; + CA6AC2632451135C00C71CB3 /* DownloadClient.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadClient.swift; sourceTree = ""; }; + CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-SharedState.swift"; sourceTree = ""; }; + CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Basics.swift"; sourceTree = ""; }; + CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-BasicsTests.swift"; sourceTree = ""; }; + CAA9ADC524465C810003A984 /* 02-Effects-Cancellation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Cancellation.swift"; sourceTree = ""; }; + CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-CancellationTests.swift"; sourceTree = ""; }; + CAA9ADC92446605B0003A984 /* 02-Effects-LongLiving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLiving.swift"; sourceTree = ""; }; + CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-LongLivingTests.swift"; sourceTree = ""; }; + CABC4F3826AEE00C00D5FA2C /* 02-Effects-Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Refreshable.swift"; sourceTree = ""; }; + CABC4F3A26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-RefreshableTests.swift"; sourceTree = ""; }; + CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-AlertsAndConfirmationDialogs.swift"; sourceTree = ""; }; + CAF069CF24ACC5AF00A1AAEF /* 00-Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "00-Core.swift"; sourceTree = ""; }; + CAF88E7024B8E26D00539345 /* tvOSCaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = tvOSCaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CAF88E7224B8E26D00539345 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + CAF88E7424B8E26D00539345 /* RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + CAF88E7624B8E26E00539345 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CAF88E7E24B8E26E00539345 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + CAF88E8324B8E26E00539345 /* tvOSCaseStudiesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = tvOSCaseStudiesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CAF88E8724B8E26E00539345 /* tvOSCaseStudiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = tvOSCaseStudiesTests.swift; sourceTree = ""; }; + CAF88E9224B8E3D000539345 /* Core.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Core.swift; sourceTree = ""; }; + CAF88E9424B8E4D500539345 /* FocusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusView.swift; sourceTree = ""; }; + DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-TimersTests.swift"; sourceTree = ""; }; + DC072321244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Sheet-LoadThenPresent.swift"; sourceTree = ""; }; + DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfLetStoreController.swift; sourceTree = ""; }; + DC25DC602450F2B000082E81 /* LoadThenNavigate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadThenNavigate.swift; sourceTree = ""; }; + DC25DC632450F2DF00082E81 /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; }; + DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-BindingBasicsTests.swift"; sourceTree = ""; }; + DC4C6EA72450DD380066A05D /* UIKitCaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIKitCaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DC4C6EAB2450DD380066A05D /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + DC4C6EAD2450DD380066A05D /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; + DC4C6EAF2450DD380066A05D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DC4C6EB22450DD380066A05D /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + DC4C6EB52450DD380066A05D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + DC4C6EB72450DD380066A05D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC4C6EBC2450DD390066A05D /* UIKitCaseStudiesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UIKitCaseStudiesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DC4C6EC02450DD390066A05D /* UIKitCaseStudiesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIKitCaseStudiesTests.swift; sourceTree = ""; }; + DC4C6EC22450DD390066A05D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC4C6ED52450E1050066A05D /* CounterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterViewController.swift; sourceTree = ""; }; + DC4C6ED72450E4570066A05D /* UIViewRepresented.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewRepresented.swift; sourceTree = ""; }; + DC4C6ED92450E6050066A05D /* NavigateAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigateAndLoad.swift; sourceTree = ""; }; + DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Bindings-Forms.swift"; sourceTree = ""; }; + DC630FD92451016B00BAECBA /* ListsOfState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsOfState.swift; sourceTree = ""; }; + DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableFavoritingTests.swift"; sourceTree = ""; }; + DC85EBC2285A731E00431CF3 /* ResignFirstResponder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResignFirstResponder.swift; sourceTree = ""; }; + DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Animations.swift"; sourceTree = ""; }; + DC89C41324460F95006900B9 /* SwiftUICaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUICaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + DC89C41A24460F95006900B9 /* 00-RootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "00-RootView.swift"; sourceTree = ""; }; + DC89C41C24460F96006900B9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + DC89C42424460F96006900B9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + DC89C42924460F96006900B9 /* SwiftUICaseStudiesTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SwiftUICaseStudiesTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + DC89C43824460FC7006900B9 /* swift-composable-architecture */ = {isa = PBXFileReference; lastKnownFileType = folder; name = "swift-composable-architecture"; path = ../..; sourceTree = ""; }; + DC89C43924460FFF006900B9 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Counter.swift"; sourceTree = ""; }; + DC89C448244618D5006900B9 /* 03-Navigation-LoadThenNavigate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-LoadThenNavigate.swift"; sourceTree = ""; }; + DC89C44C244621A5006900B9 /* 03-Navigation-NavigateAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-NavigateAndLoad.swift"; sourceTree = ""; }; + DC89C45024462DE7006900B9 /* 03-Navigation-Lists-LoadThenNavigate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Lists-LoadThenNavigate.swift"; sourceTree = ""; }; + DC89C45224465451006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Lists-NavigateAndLoad.swift"; sourceTree = ""; }; + DC89C45424465C44006900B9 /* 02-Effects-Timers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "02-Effects-Timers.swift"; sourceTree = ""; }; + DC9EB4162450CBD2005F413B /* UIViewRepresented.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIViewRepresented.swift; sourceTree = ""; }; + DCAC2A4E2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ElmLikeSubscriptions.swift"; sourceTree = ""; }; + DCC68EAA244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "03-Navigation-Sheet-PresentAndLoad.swift"; sourceTree = ""; }; + DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-OptionalState.swift"; sourceTree = ""; }; + DCC68EDE2447BC810037F998 /* TemplateText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TemplateText.swift; sourceTree = ""; }; + DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "01-GettingStarted-Composition-TwoCounters.swift"; sourceTree = ""; }; + DCC68EE22447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-ReusableFavoriting.swift"; sourceTree = ""; }; + DCD442C5286CA91F008B4EA7 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; + DCE63B70245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "04-HigherOrderReducers-Recursion.swift"; sourceTree = ""; }; + DCFE195F278DBF0600C14CCF /* CaseStudiesApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CaseStudiesApp.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CAF88E6D24B8E26D00539345 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CAF88E9124B8E3AF00539345 /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CAF88E8024B8E26E00539345 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6EA42450DD380066A05D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DC1394102469E27300EE1157 /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6EB92450DD390066A05D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C41024460F95006900B9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + DC13940E2469E25C00EE1157 /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C42624460F96006900B9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CA6AC25F2451131C00C71CB3 /* 04-HigherOrderReducers-ResuableOfflineDownloads */ = { + isa = PBXGroup; + children = ( + CA6AC2632451135C00C71CB3 /* DownloadClient.swift */, + CA6AC2622451135C00C71CB3 /* DownloadComponent.swift */, + CA6AC2602451135C00C71CB3 /* ReusableComponents-Download.swift */, + ); + path = "04-HigherOrderReducers-ResuableOfflineDownloads"; + sourceTree = ""; + }; + CAF88E7124B8E26D00539345 /* tvOSCaseStudies */ = { + isa = PBXGroup; + children = ( + CAF88E7E24B8E26E00539345 /* Info.plist */, + CAF88E7224B8E26D00539345 /* AppDelegate.swift */, + CAF88E9424B8E4D500539345 /* FocusView.swift */, + CAF88E9224B8E3D000539345 /* Core.swift */, + CAF88E7424B8E26D00539345 /* RootView.swift */, + CAF88E7624B8E26E00539345 /* Assets.xcassets */, + ); + path = tvOSCaseStudies; + sourceTree = ""; + }; + CAF88E8624B8E26E00539345 /* tvOSCaseStudiesTests */ = { + isa = PBXGroup; + children = ( + CAF88E8724B8E26E00539345 /* tvOSCaseStudiesTests.swift */, + ); + path = tvOSCaseStudiesTests; + sourceTree = ""; + }; + DC25DC622450F2D100082E81 /* Internal */ = { + isa = PBXGroup; + children = ( + DC25DC632450F2DF00082E81 /* ActivityIndicatorViewController.swift */, + DC25DC5E2450F13200082E81 /* IfLetStoreController.swift */, + DC4C6ED72450E4570066A05D /* UIViewRepresented.swift */, + ); + path = Internal; + sourceTree = ""; + }; + DC4C6EA82450DD380066A05D /* UIKitCaseStudies */ = { + isa = PBXGroup; + children = ( + DC4C6EAB2450DD380066A05D /* SceneDelegate.swift */, + DC4C6EAD2450DD380066A05D /* RootViewController.swift */, + DC4C6ED52450E1050066A05D /* CounterViewController.swift */, + DC630FD92451016B00BAECBA /* ListsOfState.swift */, + DC4C6ED92450E6050066A05D /* NavigateAndLoad.swift */, + DC25DC602450F2B000082E81 /* LoadThenNavigate.swift */, + DC25DC622450F2D100082E81 /* Internal */, + DC4C6EAF2450DD380066A05D /* Assets.xcassets */, + DC4C6EB42450DD380066A05D /* LaunchScreen.storyboard */, + DC4C6EB72450DD380066A05D /* Info.plist */, + DC4C6EB12450DD380066A05D /* Preview Content */, + ); + path = UIKitCaseStudies; + sourceTree = ""; + }; + DC4C6EB12450DD380066A05D /* Preview Content */ = { + isa = PBXGroup; + children = ( + DC4C6EB22450DD380066A05D /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + DC4C6EBF2450DD390066A05D /* UIKitCaseStudiesTests */ = { + isa = PBXGroup; + children = ( + DC4C6EC02450DD390066A05D /* UIKitCaseStudiesTests.swift */, + DC4C6EC22450DD390066A05D /* Info.plist */, + ); + path = UIKitCaseStudiesTests; + sourceTree = ""; + }; + DC89C40A24460F95006900B9 = { + isa = PBXGroup; + children = ( + DC89C43824460FC7006900B9 /* swift-composable-architecture */, + DC89C43924460FFF006900B9 /* README.md */, + DC89C41424460F95006900B9 /* Products */, + DC89C41524460F95006900B9 /* SwiftUICaseStudies */, + DC89C42C24460F96006900B9 /* SwiftUICaseStudiesTests */, + CAF88E7124B8E26D00539345 /* tvOSCaseStudies */, + CAF88E8624B8E26E00539345 /* tvOSCaseStudiesTests */, + DC4C6EA82450DD380066A05D /* UIKitCaseStudies */, + DC4C6EBF2450DD390066A05D /* UIKitCaseStudiesTests */, + ); + sourceTree = ""; + }; + DC89C41424460F95006900B9 /* Products */ = { + isa = PBXGroup; + children = ( + DC89C41324460F95006900B9 /* SwiftUICaseStudies.app */, + DC89C42924460F96006900B9 /* SwiftUICaseStudiesTests.xctest */, + DC4C6EA72450DD380066A05D /* UIKitCaseStudies.app */, + DC4C6EBC2450DD390066A05D /* UIKitCaseStudiesTests.xctest */, + CAF88E7024B8E26D00539345 /* tvOSCaseStudies.app */, + CAF88E8324B8E26E00539345 /* tvOSCaseStudiesTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + DC89C41524460F95006900B9 /* SwiftUICaseStudies */ = { + isa = PBXGroup; + children = ( + DC89C42424460F96006900B9 /* Info.plist */, + CAF069CF24ACC5AF00A1AAEF /* 00-Core.swift */, + DC89C41A24460F95006900B9 /* 00-RootView.swift */, + CAE962FC24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift */, + DC88D8A5245341EC0077F427 /* 01-GettingStarted-Animations.swift */, + CA25E5D124463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift */, + DC5B505025C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift */, + DCC68EE02447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift */, + DC89C4432446111B006900B9 /* 01-GettingStarted-Counter.swift */, + CA3E421E26B8337500581ABC /* 01-GettingStarted-FocusState.swift */, + DCC68EDC2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift */, + CA7BC8ED245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift */, + CAA9ADC12446587C0003A984 /* 02-Effects-Basics.swift */, + CAA9ADC524465C810003A984 /* 02-Effects-Cancellation.swift */, + CAA9ADC92446605B0003A984 /* 02-Effects-LongLiving.swift */, + CABC4F3826AEE00C00D5FA2C /* 02-Effects-Refreshable.swift */, + DC89C45424465C44006900B9 /* 02-Effects-Timers.swift */, + CA410EDF247A15FE00E41798 /* 02-Effects-WebSocket.swift */, + CA27C0B6245780CE00CB1E59 /* 02-Effects-SystemEnvironment.swift */, + DC89C45024462DE7006900B9 /* 03-Navigation-Lists-LoadThenNavigate.swift */, + DC89C45224465451006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift */, + DC89C448244618D5006900B9 /* 03-Navigation-LoadThenNavigate.swift */, + DC89C44C244621A5006900B9 /* 03-Navigation-NavigateAndLoad.swift */, + DC072321244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift */, + DCC68EAA244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift */, + DCAC2A4E2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift */, + CA3E4C5A24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift */, + DCE63B70245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift */, + DCC68EE22447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift */, + DCFE195F278DBF0600C14CCF /* CaseStudiesApp.swift */, + CA5ECF91267A79F0002067FF /* FactClient.swift */, + DC89C41C24460F96006900B9 /* Assets.xcassets */, + CA6AC25F2451131C00C71CB3 /* 04-HigherOrderReducers-ResuableOfflineDownloads */, + DC89C44524461416006900B9 /* Internal */, + ); + path = SwiftUICaseStudies; + sourceTree = ""; + }; + DC89C42C24460F96006900B9 /* SwiftUICaseStudiesTests */ = { + isa = PBXGroup; + children = ( + CA50BE5F24A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift */, + CA34170724A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift */, + DC27215525BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift */, + 4F5AC11E24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift */, + CAA9ADC324465AB00003A984 /* 02-Effects-BasicsTests.swift */, + CAA9ADC724465D950003A984 /* 02-Effects-CancellationTests.swift */, + CAA9ADCB2446615B0003A984 /* 02-Effects-LongLivingTests.swift */, + CABC4F3A26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift */, + DC07231624465D1E003A8B65 /* 02-Effects-TimersTests.swift */, + CA410EE1247C73B400E41798 /* 02-Effects-WebSocketTests.swift */, + CA0C0C4624B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift */, + DC634B242448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift */, + CA0C51FA245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift */, + ); + path = SwiftUICaseStudiesTests; + sourceTree = ""; + }; + DC89C44524461416006900B9 /* Internal */ = { + isa = PBXGroup; + children = ( + CA6AC2612451135C00C71CB3 /* CircularProgressView.swift */, + DC85EBC2285A731E00431CF3 /* ResignFirstResponder.swift */, + DCC68EDE2447BC810037F998 /* TemplateText.swift */, + DC9EB4162450CBD2005F413B /* UIViewRepresented.swift */, + DCD442C5286CA91F008B4EA7 /* AboutView.swift */, + ); + path = Internal; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CAF88E6F24B8E26D00539345 /* tvOSCaseStudies */ = { + isa = PBXNativeTarget; + buildConfigurationList = CAF88E8E24B8E26E00539345 /* Build configuration list for PBXNativeTarget "tvOSCaseStudies" */; + buildPhases = ( + CAF88E6C24B8E26D00539345 /* Sources */, + CAF88E6D24B8E26D00539345 /* Frameworks */, + CAF88E6E24B8E26D00539345 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = tvOSCaseStudies; + packageProductDependencies = ( + CAF88E9024B8E3AF00539345 /* ComposableArchitecture */, + ); + productName = tvOSCaseStudies; + productReference = CAF88E7024B8E26D00539345 /* tvOSCaseStudies.app */; + productType = "com.apple.product-type.application"; + }; + CAF88E8224B8E26E00539345 /* tvOSCaseStudiesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CAF88E8F24B8E26E00539345 /* Build configuration list for PBXNativeTarget "tvOSCaseStudiesTests" */; + buildPhases = ( + CAF88E7F24B8E26E00539345 /* Sources */, + CAF88E8024B8E26E00539345 /* Frameworks */, + CAF88E8124B8E26E00539345 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CAF88E8524B8E26E00539345 /* PBXTargetDependency */, + ); + name = tvOSCaseStudiesTests; + productName = tvOSCaseStudiesTests; + productReference = CAF88E8324B8E26E00539345 /* tvOSCaseStudiesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DC4C6EA62450DD380066A05D /* UIKitCaseStudies */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC4C6EC32450DD390066A05D /* Build configuration list for PBXNativeTarget "UIKitCaseStudies" */; + buildPhases = ( + DC4C6EA32450DD380066A05D /* Sources */, + DC4C6EA42450DD380066A05D /* Frameworks */, + DC4C6EA52450DD380066A05D /* Resources */, + DC4C6ECD2450E0B30066A05D /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UIKitCaseStudies; + packageProductDependencies = ( + DC13940F2469E27300EE1157 /* ComposableArchitecture */, + ); + productName = UIKitCaseStudies; + productReference = DC4C6EA72450DD380066A05D /* UIKitCaseStudies.app */; + productType = "com.apple.product-type.application"; + }; + DC4C6EBB2450DD390066A05D /* UIKitCaseStudiesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC4C6EC62450DD390066A05D /* Build configuration list for PBXNativeTarget "UIKitCaseStudiesTests" */; + buildPhases = ( + DC4C6EB82450DD390066A05D /* Sources */, + DC4C6EB92450DD390066A05D /* Frameworks */, + DC4C6EBA2450DD390066A05D /* Resources */, + DC4C6ED32450E0BA0066A05D /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + DC4C6EBE2450DD390066A05D /* PBXTargetDependency */, + ); + name = UIKitCaseStudiesTests; + packageProductDependencies = ( + ); + productName = UIKitCaseStudiesTests; + productReference = DC4C6EBC2450DD390066A05D /* UIKitCaseStudiesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + DC89C41224460F95006900B9 /* SwiftUICaseStudies */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC89C43224460F96006900B9 /* Build configuration list for PBXNativeTarget "SwiftUICaseStudies" */; + buildPhases = ( + DC89C40F24460F95006900B9 /* Sources */, + DC89C41024460F95006900B9 /* Frameworks */, + DC89C41124460F95006900B9 /* Resources */, + DC89C43D2446106D006900B9 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftUICaseStudies; + packageProductDependencies = ( + DC13940D2469E25C00EE1157 /* ComposableArchitecture */, + ); + productName = CaseStudies; + productReference = DC89C41324460F95006900B9 /* SwiftUICaseStudies.app */; + productType = "com.apple.product-type.application"; + }; + DC89C42824460F96006900B9 /* SwiftUICaseStudiesTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = DC89C43524460F96006900B9 /* Build configuration list for PBXNativeTarget "SwiftUICaseStudiesTests" */; + buildPhases = ( + DC89C42524460F96006900B9 /* Sources */, + DC89C42624460F96006900B9 /* Frameworks */, + DC89C42724460F96006900B9 /* Resources */, + DC89C44124461077006900B9 /* Embed Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + DC89C42B24460F96006900B9 /* PBXTargetDependency */, + ); + name = SwiftUICaseStudiesTests; + packageProductDependencies = ( + ); + productName = CaseStudiesTests; + productReference = DC89C42924460F96006900B9 /* SwiftUICaseStudiesTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + DC89C40B24460F95006900B9 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1150; + LastUpgradeCheck = 1240; + ORGANIZATIONNAME = "Point-Free"; + TargetAttributes = { + CAF88E6F24B8E26D00539345 = { + CreatedOnToolsVersion = 11.5; + }; + CAF88E8224B8E26E00539345 = { + CreatedOnToolsVersion = 11.5; + TestTargetID = CAF88E6F24B8E26D00539345; + }; + DC4C6EA62450DD380066A05D = { + CreatedOnToolsVersion = 11.4.1; + }; + DC4C6EBB2450DD390066A05D = { + CreatedOnToolsVersion = 11.4.1; + TestTargetID = DC4C6EA62450DD380066A05D; + }; + DC89C41224460F95006900B9 = { + CreatedOnToolsVersion = 11.4; + }; + DC89C42824460F96006900B9 = { + CreatedOnToolsVersion = 11.4; + TestTargetID = DC89C41224460F95006900B9; + }; + }; + }; + buildConfigurationList = DC89C40E24460F95006900B9 /* Build configuration list for PBXProject "CaseStudies" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = DC89C40A24460F95006900B9; + productRefGroup = DC89C41424460F95006900B9 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + DC89C41224460F95006900B9 /* SwiftUICaseStudies */, + DC89C42824460F96006900B9 /* SwiftUICaseStudiesTests */, + DC4C6EA62450DD380066A05D /* UIKitCaseStudies */, + DC4C6EBB2450DD390066A05D /* UIKitCaseStudiesTests */, + CAF88E6F24B8E26D00539345 /* tvOSCaseStudies */, + CAF88E8224B8E26E00539345 /* tvOSCaseStudiesTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CAF88E6E24B8E26D00539345 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CAF88E7724B8E26E00539345 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CAF88E8124B8E26E00539345 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6EA52450DD380066A05D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC4C6EB62450DD380066A05D /* LaunchScreen.storyboard in Resources */, + DC4C6EB32450DD380066A05D /* Preview Assets.xcassets in Resources */, + DC4C6EB02450DD380066A05D /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6EBA2450DD390066A05D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C41124460F95006900B9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC89C41D24460F96006900B9 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C42724460F96006900B9 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CAF88E6C24B8E26D00539345 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CAF88E9324B8E3D000539345 /* Core.swift in Sources */, + CAF88E9524B8E4D500539345 /* FocusView.swift in Sources */, + CAF88E7524B8E26D00539345 /* RootView.swift in Sources */, + CAF88E7324B8E26D00539345 /* AppDelegate.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CAF88E7F24B8E26E00539345 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CAF88E8824B8E26E00539345 /* tvOSCaseStudiesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6EA32450DD380066A05D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC4C6ED82450E4570066A05D /* UIViewRepresented.swift in Sources */, + DC25DC642450F2DF00082E81 /* ActivityIndicatorViewController.swift in Sources */, + DC4C6ED62450E1050066A05D /* CounterViewController.swift in Sources */, + DC4C6EDA2450E6050066A05D /* NavigateAndLoad.swift in Sources */, + DC4C6EAC2450DD380066A05D /* SceneDelegate.swift in Sources */, + DC25DC612450F2B000082E81 /* LoadThenNavigate.swift in Sources */, + DC25DC5F2450F13200082E81 /* IfLetStoreController.swift in Sources */, + DC4C6EAE2450DD380066A05D /* RootViewController.swift in Sources */, + DC630FDA2451016B00BAECBA /* ListsOfState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC4C6EB82450DD390066A05D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC4C6EC12450DD390066A05D /* UIKitCaseStudiesTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C40F24460F95006900B9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC89C449244618D5006900B9 /* 03-Navigation-LoadThenNavigate.swift in Sources */, + DCC68EE12447C4630037F998 /* 01-GettingStarted-Composition-TwoCounters.swift in Sources */, + DC072322244663B1003A8B65 /* 03-Navigation-Sheet-LoadThenPresent.swift in Sources */, + DC89C45324465452006900B9 /* 03-Navigation-Lists-NavigateAndLoad.swift in Sources */, + CAF069D024ACC5AF00A1AAEF /* 00-Core.swift in Sources */, + DCC68EE32447C8540037F998 /* 04-HigherOrderReducers-ReusableFavoriting.swift in Sources */, + CA3E421F26B8337500581ABC /* 01-GettingStarted-FocusState.swift in Sources */, + DCC68EDF2447BC810037F998 /* TemplateText.swift in Sources */, + DCAC2A4F2452352E0094DEF5 /* 04-HigherOrderReducers-ElmLikeSubscriptions.swift in Sources */, + CA6AC2672451135C00C71CB3 /* DownloadClient.swift in Sources */, + CAA9ADC624465C810003A984 /* 02-Effects-Cancellation.swift in Sources */, + CA5ECF92267A79F0002067FF /* FactClient.swift in Sources */, + DC9EB4172450CBD2005F413B /* UIViewRepresented.swift in Sources */, + CA6AC2652451135C00C71CB3 /* CircularProgressView.swift in Sources */, + CA6AC2642451135C00C71CB3 /* ReusableComponents-Download.swift in Sources */, + DCFE1960278DBF0600C14CCF /* CaseStudiesApp.swift in Sources */, + DCD442C6286CA91F008B4EA7 /* AboutView.swift in Sources */, + CA6AC2662451135C00C71CB3 /* DownloadComponent.swift in Sources */, + CA3E4C5B24B4FA0E00447C0B /* 04-HigherOrderReducers-Lifecycle.swift in Sources */, + DC5B505125C86EBC000D8DFD /* 01-GettingStarted-Bindings-Forms.swift in Sources */, + CAA9ADC22446587C0003A984 /* 02-Effects-Basics.swift in Sources */, + DC89C41B24460F95006900B9 /* 00-RootView.swift in Sources */, + DCC68EDD2447A5B00037F998 /* 01-GettingStarted-OptionalState.swift in Sources */, + CABC4F3926AEE00C00D5FA2C /* 02-Effects-Refreshable.swift in Sources */, + DC85EBC3285A731E00431CF3 /* ResignFirstResponder.swift in Sources */, + DCC68EAB244666AF0037F998 /* 03-Navigation-Sheet-PresentAndLoad.swift in Sources */, + CAE962FD24A7F7BE00EFC025 /* 01-GettingStarted-AlertsAndConfirmationDialogs.swift in Sources */, + CA25E5D224463AD700DA666A /* 01-GettingStarted-Bindings-Basics.swift in Sources */, + DC88D8A6245341EC0077F427 /* 01-GettingStarted-Animations.swift in Sources */, + DC89C44D244621A5006900B9 /* 03-Navigation-NavigateAndLoad.swift in Sources */, + DC89C4442446111B006900B9 /* 01-GettingStarted-Counter.swift in Sources */, + DCE63B71245CC0B90080A23D /* 04-HigherOrderReducers-Recursion.swift in Sources */, + CA27C0B7245780CE00CB1E59 /* 02-Effects-SystemEnvironment.swift in Sources */, + CAA9ADCA2446605B0003A984 /* 02-Effects-LongLiving.swift in Sources */, + DC89C45124462DE7006900B9 /* 03-Navigation-Lists-LoadThenNavigate.swift in Sources */, + DC89C45524465C44006900B9 /* 02-Effects-Timers.swift in Sources */, + CA410EE0247A15FE00E41798 /* 02-Effects-WebSocket.swift in Sources */, + CA7BC8EE245CCFE4001FB69F /* 01-GettingStarted-SharedState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + DC89C42524460F96006900B9 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DC27215625BF84FC00D9C8DB /* 01-GettingStarted-BindingBasicsTests.swift in Sources */, + CA0C51FB245389CC00A04EAB /* 04-HigherOrderReducers-ReusableOfflineDownloadsTests.swift in Sources */, + DC634B252448D15B00DAA016 /* 04-HigherOrderReducers-ReusableFavoritingTests.swift in Sources */, + CAA9ADC824465D950003A984 /* 02-Effects-CancellationTests.swift in Sources */, + CA410EE2247C73B400E41798 /* 02-Effects-WebSocketTests.swift in Sources */, + CABC4F3B26AEE20200D5FA2C /* 02-Effects-RefreshableTests.swift in Sources */, + CA34170824A4E89500FAF950 /* 01-GettingStarted-AnimationsTests.swift in Sources */, + CA0C0C4724B89BEC00CBDD8A /* 04-HigherOrderReducers-LifecycleTests.swift in Sources */, + DC07231724465D1E003A8B65 /* 02-Effects-TimersTests.swift in Sources */, + CA50BE6024A8F46500FE7DBA /* 01-GettingStarted-AlertsAndConfirmationDialogsTests.swift in Sources */, + CAA9ADC424465AB00003A984 /* 02-Effects-BasicsTests.swift in Sources */, + 4F5AC11F24ECC7E4009DC50B /* 01-GettingStarted-SharedStateTests.swift in Sources */, + CAA9ADCC2446615B0003A984 /* 02-Effects-LongLivingTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CAF88E8524B8E26E00539345 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CAF88E6F24B8E26D00539345 /* tvOSCaseStudies */; + targetProxy = CAF88E8424B8E26E00539345 /* PBXContainerItemProxy */; + }; + DC4C6EBE2450DD390066A05D /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DC4C6EA62450DD380066A05D /* UIKitCaseStudies */; + targetProxy = DC4C6EBD2450DD390066A05D /* PBXContainerItemProxy */; + }; + DC89C42B24460F96006900B9 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = DC89C41224460F95006900B9 /* SwiftUICaseStudies */; + targetProxy = DC89C42A24460F96006900B9 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + DC4C6EB42450DD380066A05D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + DC4C6EB52450DD380066A05D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + CAF88E8A24B8E26E00539345 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + CODE_SIGN_STYLE = Automatic; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = tvOSCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.tvOSCaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 13.3; + }; + name = Debug; + }; + CAF88E8B24B8E26E00539345 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = "App Icon & Top Shelf Image"; + CODE_SIGN_STYLE = Automatic; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = tvOSCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.tvOSCaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TVOS_DEPLOYMENT_TARGET = 13.3; + }; + name = Release; + }; + CAF88E8C24B8E26E00539345 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = tvOSCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.tvOSCaseStudiesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tvOSCaseStudies.app/tvOSCaseStudies"; + TVOS_DEPLOYMENT_TARGET = 13.3; + }; + name = Debug; + }; + CAF88E8D24B8E26E00539345 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = tvOSCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.tvOSCaseStudiesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = appletvos; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 3; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/tvOSCaseStudies.app/tvOSCaseStudies"; + TVOS_DEPLOYMENT_TARGET = 13.3; + }; + name = Release; + }; + DC4C6EC42450DD390066A05D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"UIKitCaseStudies/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = UIKitCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks"; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.UIKitCaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DC4C6EC52450DD390066A05D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"UIKitCaseStudies/Preview Content\""; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = UIKitCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks"; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.UIKitCaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + DC4C6EC72450DD390066A05D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = UIKitCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.UIKitCaseStudiesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIKitCaseStudies.app/UIKitCaseStudies"; + }; + name = Debug; + }; + DC4C6EC82450DD390066A05D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = UIKitCaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.UIKitCaseStudiesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/UIKitCaseStudies.app/UIKitCaseStudies"; + }; + name = Release; + }; + DC89C43024460F96006900B9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + DC89C43124460F96006900B9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + DC89C43324460F96006900B9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = SwiftUICaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks"; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUICaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + DC89C43424460F96006900B9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + ENABLE_PREVIEWS = YES; + INFOPLIST_FILE = SwiftUICaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + OTHER_SWIFT_FLAGS = "-Xfrontend -warn-concurrency -Xfrontend -enable-actor-data-race-checks"; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUICaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + DC89C43624460F96006900B9 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = SwiftUICaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUICaseStudiesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUICaseStudies.app/SwiftUICaseStudies"; + }; + name = Debug; + }; + DC89C43724460F96006900B9 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = SwiftUICaseStudies/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SwiftUICaseStudiesTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/SwiftUICaseStudies.app/SwiftUICaseStudies"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CAF88E8E24B8E26E00539345 /* Build configuration list for PBXNativeTarget "tvOSCaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAF88E8A24B8E26E00539345 /* Debug */, + CAF88E8B24B8E26E00539345 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CAF88E8F24B8E26E00539345 /* Build configuration list for PBXNativeTarget "tvOSCaseStudiesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAF88E8C24B8E26E00539345 /* Debug */, + CAF88E8D24B8E26E00539345 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC4C6EC32450DD390066A05D /* Build configuration list for PBXNativeTarget "UIKitCaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC4C6EC42450DD390066A05D /* Debug */, + DC4C6EC52450DD390066A05D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC4C6EC62450DD390066A05D /* Build configuration list for PBXNativeTarget "UIKitCaseStudiesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC4C6EC72450DD390066A05D /* Debug */, + DC4C6EC82450DD390066A05D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC89C40E24460F95006900B9 /* Build configuration list for PBXProject "CaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC89C43024460F96006900B9 /* Debug */, + DC89C43124460F96006900B9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC89C43224460F96006900B9 /* Build configuration list for PBXNativeTarget "SwiftUICaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC89C43324460F96006900B9 /* Debug */, + DC89C43424460F96006900B9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + DC89C43524460F96006900B9 /* Build configuration list for PBXNativeTarget "SwiftUICaseStudiesTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + DC89C43624460F96006900B9 /* Debug */, + DC89C43724460F96006900B9 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCSwiftPackageProductDependency section */ + CAF88E9024B8E3AF00539345 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; + DC13940D2469E25C00EE1157 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; + DC13940F2469E27300EE1157 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = DC89C40B24460F95006900B9 /* Project object */; +} diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100755 index 0000000..919434a --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100755 index 0000000..18d9810 --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (SwiftUI).xcscheme b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (SwiftUI).xcscheme new file mode 100755 index 0000000..7cd4593 --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (SwiftUI).xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (UIKit).xcscheme b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (UIKit).xcscheme new file mode 100755 index 0000000..50d8ea9 --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies (UIKit).xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/tvOSCaseStudies.xcscheme b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/tvOSCaseStudies.xcscheme new file mode 100755 index 0000000..15b2751 --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/tvOSCaseStudies.xcscheme @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/CaseStudies/README.md b/Examples/CaseStudies/README.md new file mode 100755 index 0000000..0468b9f --- /dev/null +++ b/Examples/CaseStudies/README.md @@ -0,0 +1,3 @@ +# Composable Architecture Case Studies + +This project includes a number of digestible examples of how to solve common problems using the Composable Architecture. diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift new file mode 100755 index 0000000..f364822 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-Core.swift @@ -0,0 +1,283 @@ +import Combine +import ComposableArchitecture +import UIKit +import XCTestDynamicOverlay + +struct RootState: Equatable { + var alertAndConfirmationDialog = AlertAndConfirmationDialogState() + var animation = AnimationsState() + var bindingBasics = BindingBasicsState() + var bindingForm = BindingFormState() + var clock = ClockState() + var counter = CounterState() + var effectsBasics = EffectsBasicsState() + var effectsCancellation = EffectsCancellationState() + var effectsTimers = TimersState() + var episodes = EpisodesState(episodes: .mocks) + var focusDemo = FocusDemoState() + var lifecycle = LifecycleDemoState() + var loadThenNavigate = LoadThenNavigateState() + var loadThenNavigateList = LoadThenNavigateListState() + var loadThenPresent = LoadThenPresentState() + var longLivingEffects = LongLivingEffectsState() + var map = MapAppState(cityMaps: .mocks) + var multipleDependencies = MultipleDependenciesState() + var navigateAndLoad = NavigateAndLoadState() + var navigateAndLoadList = NavigateAndLoadListState() + var nested = NestedState.mock + var optionalBasics = OptionalBasicsState() + var presentAndLoad = PresentAndLoadState() + var refreshable = RefreshableState() + var shared = SharedState() + var timers = TimersState() + var twoCounters = TwoCountersState() + var webSocket = WebSocketState() +} + +enum RootAction { + case alertAndConfirmationDialog(AlertAndConfirmationDialogAction) + case animation(AnimationsAction) + case bindingBasics(BindingBasicsAction) + case bindingForm(BindingFormAction) + case clock(ClockAction) + case counter(CounterAction) + case effectsBasics(EffectsBasicsAction) + case effectsCancellation(EffectsCancellationAction) + case episodes(EpisodesAction) + case focusDemo(FocusDemoAction) + case lifecycle(LifecycleDemoAction) + case loadThenNavigate(LoadThenNavigateAction) + case loadThenNavigateList(LoadThenNavigateListAction) + case loadThenPresent(LoadThenPresentAction) + case longLivingEffects(LongLivingEffectsAction) + case map(MapAppAction) + case multipleDependencies(MultipleDependenciesAction) + case navigateAndLoad(NavigateAndLoadAction) + case navigateAndLoadList(NavigateAndLoadListAction) + case nested(NestedAction) + case optionalBasics(OptionalBasicsAction) + case onAppear + case presentAndLoad(PresentAndLoadAction) + case refreshable(RefreshableAction) + case shared(SharedStateAction) + case timers(TimersAction) + case twoCounters(TwoCountersAction) + case webSocket(WebSocketAction) +} + +struct RootEnvironment { + var date: @Sendable () -> Date + var downloadClient: DownloadClient + var fact: FactClient + var favorite: @Sendable (UUID, Bool) async throws -> Bool + var fetchNumber: @Sendable () async throws -> Int + var mainQueue: AnySchedulerOf + var screenshots: @Sendable () async -> AsyncStream + var uuid: @Sendable () -> UUID + var webSocket: WebSocketClient + + static let live = Self( + date: { Date() }, + downloadClient: .live, + fact: .live, + favorite: favorite(id:isFavorite:), + fetchNumber: liveFetchNumber, + mainQueue: .main, + screenshots: { @MainActor in + AsyncStream( + NotificationCenter.default + .notifications(named: UIApplication.userDidTakeScreenshotNotification) + .map { _ in } + ) + }, + uuid: { UUID() }, + webSocket: .live + ) +} + +let rootReducer = Reducer.combine( + .init { state, action, _ in + switch action { + case .onAppear: + state = .init() + return .none + + default: + return .none + } + }, + alertAndConfirmationDialogReducer + .pullback( + state: \.alertAndConfirmationDialog, + action: /RootAction.alertAndConfirmationDialog, + environment: { _ in .init() } + ), + animationsReducer + .pullback( + state: \.animation, + action: /RootAction.animation, + environment: { .init(mainQueue: $0.mainQueue) } + ), + bindingBasicsReducer + .pullback( + state: \.bindingBasics, + action: /RootAction.bindingBasics, + environment: { _ in .init() } + ), + bindingFormReducer + .pullback( + state: \.bindingForm, + action: /RootAction.bindingForm, + environment: { _ in .init() } + ), + clockReducer + .pullback( + state: \.clock, + action: /RootAction.clock, + environment: { .init(mainQueue: $0.mainQueue) } + ), + counterReducer + .pullback( + state: \.counter, + action: /RootAction.counter, + environment: { _ in .init() } + ), + effectsBasicsReducer + .pullback( + state: \.effectsBasics, + action: /RootAction.effectsBasics, + environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) } + ), + effectsCancellationReducer + .pullback( + state: \.effectsCancellation, + action: /RootAction.effectsCancellation, + environment: { .init(fact: $0.fact) } + ), + episodesReducer + .pullback( + state: \.episodes, + action: /RootAction.episodes, + environment: { .init(favorite: $0.favorite) } + ), + focusDemoReducer + .pullback( + state: \.focusDemo, + action: /RootAction.focusDemo, + environment: { _ in .init() } + ), + lifecycleDemoReducer + .pullback( + state: \.lifecycle, + action: /RootAction.lifecycle, + environment: { .init(mainQueue: $0.mainQueue) } + ), + loadThenNavigateReducer + .pullback( + state: \.loadThenNavigate, + action: /RootAction.loadThenNavigate, + environment: { .init(mainQueue: $0.mainQueue) } + ), + loadThenNavigateListReducer + .pullback( + state: \.loadThenNavigateList, + action: /RootAction.loadThenNavigateList, + environment: { .init(mainQueue: $0.mainQueue) } + ), + loadThenPresentReducer + .pullback( + state: \.loadThenPresent, + action: /RootAction.loadThenPresent, + environment: { .init(mainQueue: $0.mainQueue) } + ), + longLivingEffectsReducer + .pullback( + state: \.longLivingEffects, + action: /RootAction.longLivingEffects, + environment: { .init(screenshots: $0.screenshots) } + ), + mapAppReducer + .pullback( + state: \.map, + action: /RootAction.map, + environment: { .init(downloadClient: $0.downloadClient, mainQueue: $0.mainQueue) } + ), + multipleDependenciesReducer + .pullback( + state: \.multipleDependencies, + action: /RootAction.multipleDependencies, + environment: { env in + .init( + date: env.date, + environment: .init(fetchNumber: env.fetchNumber), + mainQueue: env.mainQueue, + uuid: env.uuid + ) + } + ), + navigateAndLoadReducer + .pullback( + state: \.navigateAndLoad, + action: /RootAction.navigateAndLoad, + environment: { .init(mainQueue: $0.mainQueue) } + ), + navigateAndLoadListReducer + .pullback( + state: \.navigateAndLoadList, + action: /RootAction.navigateAndLoadList, + environment: { .init(mainQueue: $0.mainQueue) } + ), + nestedReducer + .pullback( + state: \.nested, + action: /RootAction.nested, + environment: { .init(uuid: $0.uuid) } + ), + optionalBasicsReducer + .pullback( + state: \.optionalBasics, + action: /RootAction.optionalBasics, + environment: { _ in .init() } + ), + presentAndLoadReducer + .pullback( + state: \.presentAndLoad, + action: /RootAction.presentAndLoad, + environment: { .init(mainQueue: $0.mainQueue) } + ), + refreshableReducer + .pullback( + state: \.refreshable, + action: /RootAction.refreshable, + environment: { .init(fact: $0.fact, mainQueue: $0.mainQueue) } + ), + sharedStateReducer + .pullback( + state: \.shared, + action: /RootAction.shared, + environment: { _ in () } + ), + timersReducer + .pullback( + state: \.timers, + action: /RootAction.timers, + environment: { .init(mainQueue: $0.mainQueue) } + ), + twoCountersReducer + .pullback( + state: \.twoCounters, + action: /RootAction.twoCounters, + environment: { _ in .init() } + ), + webSocketReducer + .pullback( + state: \.webSocket, + action: /RootAction.webSocket, + environment: { .init(mainQueue: $0.mainQueue, webSocket: $0.webSocket) } + ) +) + +@Sendable private func liveFetchNumber() async throws -> Int { + try await Task.sleep(nanoseconds: NSEC_PER_SEC) + return Int.random(in: 1...1_000) +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift new file mode 100755 index 0000000..a028f4e --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/00-RootView.swift @@ -0,0 +1,304 @@ +import Combine +import ComposableArchitecture +import SwiftUI + +struct RootView: View { + let store: Store + + var body: some View { + NavigationView { + Form { + Section(header: Text("Getting started")) { + NavigationLink( + "Basics", + destination: CounterDemoView( + store: self.store.scope( + state: \.counter, + action: RootAction.counter + ) + ) + ) + + NavigationLink( + "Pullback and combine", + destination: TwoCountersView( + store: self.store.scope( + state: \.twoCounters, + action: RootAction.twoCounters + ) + ) + ) + + NavigationLink( + "Bindings", + destination: BindingBasicsView( + store: self.store.scope( + state: \.bindingBasics, + action: RootAction.bindingBasics + ) + ) + ) + + NavigationLink( + "Form bindings", + destination: BindingFormView( + store: self.store.scope( + state: \.bindingForm, + action: RootAction.bindingForm + ) + ) + ) + + NavigationLink( + "Optional state", + destination: OptionalBasicsView( + store: self.store.scope( + state: \.optionalBasics, + action: RootAction.optionalBasics + ) + ) + ) + + NavigationLink( + "Shared state", + destination: SharedStateView( + store: self.store.scope( + state: \.shared, + action: RootAction.shared + ) + ) + ) + + NavigationLink( + "Alerts and Confirmation Dialogs", + destination: AlertAndConfirmationDialogView( + store: self.store.scope( + state: \.alertAndConfirmationDialog, + action: RootAction.alertAndConfirmationDialog + ) + ) + ) + + NavigationLink( + "Focus State", + destination: FocusDemoView( + store: self.store.scope( + state: \.focusDemo, + action: RootAction.focusDemo + ) + ) + ) + + NavigationLink( + "Animations", + destination: AnimationsView( + store: self.store.scope( + state: \.animation, + action: RootAction.animation + ) + ) + ) + } + + Section(header: Text("Effects")) { + NavigationLink( + "Basics", + destination: EffectsBasicsView( + store: self.store.scope( + state: \.effectsBasics, + action: RootAction.effectsBasics + ) + ) + ) + + NavigationLink( + "Cancellation", + destination: EffectsCancellationView( + store: self.store.scope( + state: \.effectsCancellation, + action: RootAction.effectsCancellation) + ) + ) + + NavigationLink( + "Long-living effects", + destination: LongLivingEffectsView( + store: self.store.scope( + state: \.longLivingEffects, + action: RootAction.longLivingEffects + ) + ) + ) + + NavigationLink( + "Refreshable", + destination: RefreshableView( + store: self.store.scope( + state: \.refreshable, + action: RootAction.refreshable + ) + ) + ) + + NavigationLink( + "Timers", + destination: TimersView( + store: self.store.scope( + state: \.timers, + action: RootAction.timers + ) + ) + ) + + NavigationLink( + "System environment", + destination: MultipleDependenciesView( + store: self.store.scope( + state: \.multipleDependencies, + action: RootAction.multipleDependencies + ) + ) + ) + + NavigationLink( + "Web socket", + destination: WebSocketView( + store: self.store.scope( + state: \.webSocket, + action: RootAction.webSocket + ) + ) + ) + } + + Section(header: Text("Navigation")) { + NavigationLink( + "Navigate and load data", + destination: NavigateAndLoadView( + store: self.store.scope( + state: \.navigateAndLoad, + action: RootAction.navigateAndLoad + ) + ) + ) + + NavigationLink( + "Load data then navigate", + destination: LoadThenNavigateView( + store: self.store.scope( + state: \.loadThenNavigate, + action: RootAction.loadThenNavigate + ) + ) + ) + + NavigationLink( + "Lists: Navigate and load data", + destination: NavigateAndLoadListView( + store: self.store.scope( + state: \.navigateAndLoadList, + action: RootAction.navigateAndLoadList + ) + ) + ) + + NavigationLink( + "Lists: Load data then navigate", + destination: LoadThenNavigateListView( + store: self.store.scope( + state: \.loadThenNavigateList, + action: RootAction.loadThenNavigateList + ) + ) + ) + + NavigationLink( + "Sheets: Present and load data", + destination: PresentAndLoadView( + store: self.store.scope( + state: \.presentAndLoad, + action: RootAction.presentAndLoad + ) + ) + ) + + NavigationLink( + "Sheets: Load data then present", + destination: LoadThenPresentView( + store: self.store.scope( + state: \.loadThenPresent, + action: RootAction.loadThenPresent + ) + ) + ) + } + + Section(header: Text("Higher-order reducers")) { + NavigationLink( + "Reusable favoriting component", + destination: EpisodesView( + store: self.store.scope( + state: \.episodes, + action: RootAction.episodes + ) + ) + ) + + NavigationLink( + "Reusable offline download component", + destination: CitiesView( + store: self.store.scope( + state: \.map, + action: RootAction.map + ) + ) + ) + + NavigationLink( + "Lifecycle", + destination: LifecycleDemoView( + store: self.store.scope( + state: \.lifecycle, + action: RootAction.lifecycle + ) + ) + ) + + NavigationLink( + "Elm-like subscriptions", + destination: ClockView( + store: self.store.scope( + state: \.clock, + action: RootAction.clock + ) + ) + ) + + NavigationLink( + "Recursive state and actions", + destination: NestedView( + store: self.store.scope( + state: \.nested, + action: RootAction.nested + ) + ) + ) + } + } + .navigationTitle("Case Studies") + .onAppear { ViewStore(self.store).send(.onAppear) } + } + } +} + +struct RootView_Previews: PreviewProvider { + static var previews: some View { + RootView( + store: Store( + initialState: RootState(), + reducer: rootReducer, + environment: .live + ) + ) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift new file mode 100755 index 0000000..9fd31c0 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-AlertsAndConfirmationDialogs.swift @@ -0,0 +1,125 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This demonstrates how to best handle alerts and confirmation dialogs in the Composable \ + Architecture. + + Because the library demands that all data flow through the application in a single direction, we \ + cannot leverage SwiftUI's two-way bindings because they can make changes to state without going \ + through a reducer. This means we can't directly use the standard API to display alerts and sheets. + + However, the library comes with two types, `AlertState` and `ConfirmationDialogState`, which can \ + be constructed from reducers and control whether or not an alert or confirmation dialog is \ + displayed. Further, it automatically handles sending actions when you tap their buttons, which \ + allows you to properly handle their functionality in the reducer rather than in two-way bindings \ + and action closures. + + The benefit of doing this is that you can get full test coverage on how a user interacts with \ + alerts and dialogs in your application + """ + +struct AlertAndConfirmationDialogState: Equatable { + var alert: AlertState? + var confirmationDialog: ConfirmationDialogState? + var count = 0 +} + +enum AlertAndConfirmationDialogAction: Equatable { + case alertButtonTapped + case alertDismissed + case confirmationDialogButtonTapped + case confirmationDialogDismissed + case decrementButtonTapped + case incrementButtonTapped +} + +struct AlertAndConfirmationDialogEnvironment {} + +let alertAndConfirmationDialogReducer = Reducer< + AlertAndConfirmationDialogState, AlertAndConfirmationDialogAction, + AlertAndConfirmationDialogEnvironment +> { state, action, _ in + + switch action { + case .alertButtonTapped: + state.alert = AlertState( + title: TextState("Alert!"), + message: TextState("This is an alert"), + primaryButton: .cancel(TextState("Cancel")), + secondaryButton: .default(TextState("Increment"), action: .send(.incrementButtonTapped)) + ) + return .none + + case .alertDismissed: + state.alert = nil + return .none + + case .confirmationDialogButtonTapped: + state.confirmationDialog = ConfirmationDialogState( + title: TextState("Confirmation dialog"), + message: TextState("This is a confirmation dialog."), + buttons: [ + .cancel(TextState("Cancel")), + .default(TextState("Increment"), action: .send(.incrementButtonTapped)), + .default(TextState("Decrement"), action: .send(.decrementButtonTapped)), + ] + ) + return .none + + case .confirmationDialogDismissed: + state.confirmationDialog = nil + return .none + + case .decrementButtonTapped: + state.alert = AlertState(title: TextState("Decremented!")) + state.count -= 1 + return .none + + case .incrementButtonTapped: + state.alert = AlertState(title: TextState("Incremented!")) + state.count += 1 + return .none + } +} + +struct AlertAndConfirmationDialogView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + Text("Count: \(viewStore.count)") + Button("Alert") { viewStore.send(.alertButtonTapped) } + Button("Confirmation Dialog") { viewStore.send(.confirmationDialogButtonTapped) } + } + } + .navigationTitle("Alerts & Dialogs") + .alert( + self.store.scope(state: \.alert), + dismiss: .alertDismissed + ) + .confirmationDialog( + self.store.scope(state: \.confirmationDialog), + dismiss: .confirmationDialogDismissed + ) + } +} + +struct AlertAndConfirmationDialog_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + AlertAndConfirmationDialogView( + store: Store( + initialState: AlertAndConfirmationDialogState(), + reducer: alertAndConfirmationDialogReducer, + environment: AlertAndConfirmationDialogEnvironment() + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift new file mode 100755 index 0000000..9aaedd7 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Animations.swift @@ -0,0 +1,170 @@ +import Combine +import ComposableArchitecture +@preconcurrency import SwiftUI // NB: SwiftUI.Color and SwiftUI.Animation are not Sendable yet. + +private let readMe = """ + This screen demonstrates how changes to application state can drive animations. Because the \ + `Store` processes actions sent to it synchronously you can typically perform animations \ + in the Composable Architecture just as you would in regular SwiftUI. + + To animate the changes made to state when an action is sent to the store you can pass along an \ + explicit animation, as well, or you can call `viewStore.send` in a `withAnimation` block. + + To animate changes made to state through a binding, use the `.animation` method on `Binding`. + + To animate asynchronous changes made to state via effects, use the `.animation` method provided \ + by the CombineSchedulers library to receive asynchronous actions in an animated fashion. + + Try it out by tapping or dragging anywhere on the screen to move the dot, and by flipping the \ + toggle at the bottom of the screen. + """ + +struct AnimationsState: Equatable { + var alert: AlertState? + var circleCenter: CGPoint? + var circleColor = Color.black + var isCircleScaled = false +} + +enum AnimationsAction: Equatable, Sendable { + case alertDismissed + case circleScaleToggleChanged(Bool) + case rainbowButtonTapped + case resetButtonTapped + case resetConfirmationButtonTapped + case setColor(Color) + case tapped(CGPoint) +} + +struct AnimationsEnvironment { + var mainQueue: AnySchedulerOf +} + +let animationsReducer = Reducer { + state, action, environment in + enum CancelID {} + + switch action { + case .alertDismissed: + state.alert = nil + return .none + + case let .circleScaleToggleChanged(isScaled): + state.isCircleScaled = isScaled + return .none + + case .rainbowButtonTapped: + return .run { send in + for color in [Color.red, .blue, .green, .orange, .pink, .purple, .yellow, .black] { + await send(.setColor(color), animation: .linear) + try await environment.mainQueue.sleep(for: 1) + } + } + .cancellable(id: CancelID.self) + + case .resetButtonTapped: + state.alert = AlertState( + title: TextState("Reset state?"), + primaryButton: .destructive( + TextState("Reset"), + action: .send(.resetConfirmationButtonTapped, animation: .default) + ), + secondaryButton: .cancel(TextState("Cancel")) + ) + return .none + + case .resetConfirmationButtonTapped: + state = AnimationsState() + return .cancel(id: CancelID.self) + + case let .setColor(color): + state.circleColor = color + return .none + + case let .tapped(point): + state.circleCenter = point + return .none + } +} + +struct AnimationsView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(alignment: .leading) { + Text(template: readMe, .body) + .padding() + .gesture( + DragGesture(minimumDistance: 0).onChanged { gesture in + viewStore.send( + .tapped(gesture.location), + animation: .interactiveSpring(response: 0.25, dampingFraction: 0.1) + ) + } + ) + .overlay { + GeometryReader { proxy in + Circle() + .fill(viewStore.circleColor) + .colorInvert() + .blendMode(.difference) + .frame(width: 50, height: 50) + .scaleEffect(viewStore.isCircleScaled ? 2 : 1) + .position( + x: viewStore.circleCenter?.x ?? proxy.size.width / 2, + y: viewStore.circleCenter?.y ?? proxy.size.height / 2 + ) + .offset(y: viewStore.circleCenter == nil ? 0 : -44) + } + .allowsHitTesting(false) + } + Toggle( + "Big mode", + isOn: + viewStore + .binding(get: \.isCircleScaled, send: AnimationsAction.circleScaleToggleChanged) + .animation(.interactiveSpring(response: 0.25, dampingFraction: 0.1)) + ) + .padding() + Button("Rainbow") { viewStore.send(.rainbowButtonTapped, animation: .linear) } + .padding([.horizontal, .bottom]) + Button("Reset") { viewStore.send(.resetButtonTapped) } + .padding([.horizontal, .bottom]) + } + .alert(self.store.scope(state: \.alert), dismiss: .alertDismissed) + .navigationBarTitleDisplayMode(.inline) + } + } +} + +struct AnimationsView_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationView { + AnimationsView( + store: Store( + initialState: AnimationsState(), + reducer: animationsReducer, + environment: AnimationsEnvironment( + mainQueue: .main + ) + ) + ) + } + + NavigationView { + AnimationsView( + store: Store( + initialState: AnimationsState(), + reducer: animationsReducer, + environment: AnimationsEnvironment( + mainQueue: .main + ) + ) + ) + } + .environment(\.colorScheme, .dark) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Basics.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Basics.swift new file mode 100755 index 0000000..78928f7 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Basics.swift @@ -0,0 +1,138 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This file demonstrates how to handle two-way bindings in the Composable Architecture. + + Two-way bindings in SwiftUI are powerful, but also go against the grain of the "unidirectional \ + data flow" of the Composable Architecture. This is because anything can mutate the value \ + whenever it wants. + + On the other hand, the Composable Architecture demands that mutations can only happen by sending \ + actions to the store, and this means there is only ever one place to see how the state of our \ + feature evolves, which is the reducer. + + Any SwiftUI component that requires a Binding to do its job can be used in the Composable \ + Architecture. You can derive a Binding from your ViewStore by using the `binding` method. This \ + will allow you to specify what state renders the component, and what action to send when the \ + component changes, which means you can keep using a unidirectional style for your feature. + """ + +// The state for this screen holds a bunch of values that will drive +struct BindingBasicsState: Equatable { + var sliderValue = 5.0 + var stepCount = 10 + var text = "" + var toggleIsOn = false +} + +enum BindingBasicsAction { + case sliderValueChanged(Double) + case stepCountChanged(Int) + case textChanged(String) + case toggleChanged(isOn: Bool) +} + +struct BindingBasicsEnvironment {} + +let bindingBasicsReducer = Reducer< + BindingBasicsState, BindingBasicsAction, BindingBasicsEnvironment +> { + state, action, _ in + switch action { + case let .sliderValueChanged(value): + state.sliderValue = value + return .none + + case let .stepCountChanged(count): + state.sliderValue = .minimum(state.sliderValue, Double(count)) + state.stepCount = count + return .none + + case let .textChanged(text): + state.text = text + return .none + + case let .toggleChanged(isOn): + state.toggleIsOn = isOn + return .none + } +} + +struct BindingBasicsView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + HStack { + TextField( + "Type here", + text: viewStore.binding(get: \.text, send: BindingBasicsAction.textChanged) + ) + .disableAutocorrection(true) + .foregroundStyle(viewStore.toggleIsOn ? Color.secondary : .primary) + Text(alternate(viewStore.text)) + } + .disabled(viewStore.toggleIsOn) + + Toggle( + "Disable other controls", + isOn: viewStore.binding(get: \.toggleIsOn, send: BindingBasicsAction.toggleChanged) + .resignFirstResponder() + ) + + Stepper( + "Max slider value: \(viewStore.stepCount)", + value: viewStore.binding(get: \.stepCount, send: BindingBasicsAction.stepCountChanged), + in: 0...100 + ) + .disabled(viewStore.toggleIsOn) + + HStack { + Text("Slider value: \(Int(viewStore.sliderValue))") + Slider( + value: viewStore.binding( + get: \.sliderValue, + send: BindingBasicsAction.sliderValueChanged + ), + in: 0...Double(viewStore.stepCount) + ) + .tint(.accentColor) + } + .disabled(viewStore.toggleIsOn) + } + } + .monospacedDigit() + .navigationTitle("Bindings basics") + } +} + +private func alternate(_ string: String) -> String { + string + .enumerated() + .map { idx, char in + idx.isMultiple(of: 2) + ? char.uppercased() + : char.lowercased() + } + .joined() +} + +struct BindingBasicsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + BindingBasicsView( + store: Store( + initialState: BindingBasicsState(), + reducer: bindingBasicsReducer, + environment: BindingBasicsEnvironment() + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift new file mode 100755 index 0000000..79f4e74 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Bindings-Forms.swift @@ -0,0 +1,123 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This file demonstrates how to handle two-way bindings in the Composable Architecture using \ + bindable state and actions. + + Bindable state and actions allow you to safely eliminate the boilerplate caused by needing to \ + have a unique action for every UI control. Instead, all UI bindings can be consolidated into a \ + single `binding` action that holds onto a `BindingAction` value, and all bindable state can be \ + safeguarded with the `BindableState` property wrapper. + + It is instructive to compare this case study to the "Binding Basics" case study. + """ + +// The state for this screen holds a bunch of values that will drive +struct BindingFormState: Equatable { + @BindableState var sliderValue = 5.0 + @BindableState var stepCount = 10 + @BindableState var text = "" + @BindableState var toggleIsOn = false +} + +enum BindingFormAction: BindableAction, Equatable { + case binding(BindingAction) + case resetButtonTapped +} + +struct BindingFormEnvironment {} + +let bindingFormReducer = Reducer< + BindingFormState, BindingFormAction, BindingFormEnvironment +> { + state, action, _ in + switch action { + case .binding(\.$stepCount): + state.sliderValue = .minimum(state.sliderValue, Double(state.stepCount)) + return .none + + case .binding: + return .none + + case .resetButtonTapped: + state = BindingFormState() + return .none + } +} +.binding() + +struct BindingFormView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + HStack { + TextField("Type here", text: viewStore.binding(\.$text)) + .disableAutocorrection(true) + .foregroundStyle(viewStore.toggleIsOn ? Color.secondary : .primary) + Text(alternate(viewStore.text)) + } + .disabled(viewStore.toggleIsOn) + + Toggle( + "Disable other controls", + isOn: viewStore.binding(\.$toggleIsOn) + .resignFirstResponder() + ) + + Stepper( + "Max slider value: \(viewStore.stepCount)", + value: viewStore.binding(\.$stepCount), + in: 0...100 + ) + .disabled(viewStore.toggleIsOn) + + HStack { + Text("Slider value: \(Int(viewStore.sliderValue))") + + Slider(value: viewStore.binding(\.$sliderValue), in: 0...Double(viewStore.stepCount)) + .tint(.accentColor) + } + .disabled(viewStore.toggleIsOn) + + Button("Reset") { + viewStore.send(.resetButtonTapped) + } + .tint(.red) + } + } + .monospacedDigit() + .navigationTitle("Bindings form") + } +} + +private func alternate(_ string: String) -> String { + string + .enumerated() + .map { idx, char in + idx.isMultiple(of: 2) + ? char.uppercased() + : char.lowercased() + } + .joined() +} + +struct BindingFormView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + BindingFormView( + store: Store( + initialState: BindingFormState(), + reducer: bindingFormReducer, + environment: BindingFormEnvironment() + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Composition-TwoCounters.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Composition-TwoCounters.swift new file mode 100755 index 0000000..3a9af1e --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Composition-TwoCounters.swift @@ -0,0 +1,79 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how to take small features and compose them into bigger ones using the \ + `pullback` and `combine` operators on reducers, and the `scope` operator on stores. + + It reuses the the domain of the counter screen and embeds it, twice, in a larger domain. + """ + +struct TwoCountersState: Equatable { + var counter1 = CounterState() + var counter2 = CounterState() +} + +enum TwoCountersAction { + case counter1(CounterAction) + case counter2(CounterAction) +} + +struct TwoCountersEnvironment {} + +let twoCountersReducer = Reducer + .combine( + counterReducer.pullback( + state: \TwoCountersState.counter1, + action: /TwoCountersAction.counter1, + environment: { _ in CounterEnvironment() } + ), + counterReducer.pullback( + state: \TwoCountersState.counter2, + action: /TwoCountersAction.counter2, + environment: { _ in CounterEnvironment() } + ) + ) + +struct TwoCountersView: View { + let store: Store + + var body: some View { + Form { + Section { + AboutView(readMe: readMe) + } + + HStack { + Text("Counter 1") + Spacer() + CounterView( + store: self.store.scope(state: \.counter1, action: TwoCountersAction.counter1) + ) + } + + HStack { + Text("Counter 2") + Spacer() + CounterView( + store: self.store.scope(state: \.counter2, action: TwoCountersAction.counter2) + ) + } + } + .buttonStyle(.borderless) + .navigationTitle("Two counter demo") + } +} + +struct TwoCountersView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + TwoCountersView( + store: Store( + initialState: TwoCountersState(), + reducer: twoCountersReducer, + environment: TwoCountersEnvironment() + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Counter.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Counter.swift new file mode 100755 index 0000000..ccbdeee --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-Counter.swift @@ -0,0 +1,90 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates the basics of the Composable Architecture in an archetypal counter \ + application. + + The domain of the application is modeled using simple data types that correspond to the mutable \ + state of the application and any actions that can affect that state or the outside world. + """ + +struct CounterState: Equatable { + var count = 0 +} + +enum CounterAction: Equatable { + case decrementButtonTapped + case incrementButtonTapped +} + +struct CounterEnvironment {} + +let counterReducer = Reducer { state, action, _ in + switch action { + case .decrementButtonTapped: + state.count -= 1 + return .none + case .incrementButtonTapped: + state.count += 1 + return .none + } +} + +struct CounterView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + HStack { + Button { + viewStore.send(.decrementButtonTapped) + } label: { + Image(systemName: "minus") + } + + Text("\(viewStore.count)") + .monospacedDigit() + + Button { + viewStore.send(.incrementButtonTapped) + } label: { + Image(systemName: "plus") + } + } + } + } +} + +struct CounterDemoView: View { + let store: Store + + var body: some View { + Form { + Section { + AboutView(readMe: readMe) + } + + Section { + CounterView(store: self.store) + .frame(maxWidth: .infinity) + } + } + .buttonStyle(.borderless) + .navigationTitle("Counter demo") + } +} + +struct CounterView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + CounterDemoView( + store: Store( + initialState: CounterState(), + reducer: counterReducer, + environment: CounterEnvironment() + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-FocusState.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-FocusState.swift new file mode 100755 index 0000000..6b3d498 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-FocusState.swift @@ -0,0 +1,97 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This demonstrates how to make use of SwiftUI's `@FocusState` in the Composable Architecture. \ + If you tap the "Sign in" button while a field is empty, the focus will be changed to that field. + """ + +struct FocusDemoState: Equatable { + @BindableState var focusedField: Field? + @BindableState var password: String = "" + @BindableState var username: String = "" + + enum Field: String, Hashable { + case username, password + } +} + +enum FocusDemoAction: BindableAction, Equatable { + case binding(BindingAction) + case signInButtonTapped +} + +struct FocusDemoEnvironment {} + +let focusDemoReducer = Reducer< + FocusDemoState, + FocusDemoAction, + FocusDemoEnvironment +> { state, action, _ in + switch action { + case .binding: + return .none + + case .signInButtonTapped: + if state.username.isEmpty { + state.focusedField = .username + } else if state.password.isEmpty { + state.focusedField = .password + } + return .none + } +} +.binding() + +struct FocusDemoView: View { + let store: Store + @FocusState var focusedField: FocusDemoState.Field? + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + AboutView(readMe: readMe) + + VStack { + TextField("Username", text: viewStore.binding(\.$username)) + .focused($focusedField, equals: .username) + + SecureField("Password", text: viewStore.binding(\.$password)) + .focused($focusedField, equals: .password) + Button("Sign In") { + viewStore.send(.signInButtonTapped) + } + .buttonStyle(.borderedProminent) + } + .textFieldStyle(.roundedBorder) + } + .synchronize(viewStore.binding(\.$focusedField), self.$focusedField) + } + .navigationTitle("Focus demo") + } +} + +extension View { + func synchronize( + _ first: Binding, + _ second: FocusState.Binding + ) -> some View { + self + .onChange(of: first.wrappedValue) { second.wrappedValue = $0 } + .onChange(of: second.wrappedValue) { first.wrappedValue = $0 } + } +} + +struct FocusDemo_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + FocusDemoView( + store: Store( + initialState: FocusDemoState(), + reducer: focusDemoReducer, + environment: FocusDemoEnvironment() + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-OptionalState.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-OptionalState.swift new file mode 100755 index 0000000..ecb6af4 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-OptionalState.swift @@ -0,0 +1,110 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how to show and hide views based on the presence of some optional child \ + state. + + The parent state holds a `CounterState?` value. When it is `nil` we will default to a plain text \ + view. But when it is non-`nil` we will show a view fragment for a counter that operates on the \ + non-optional counter state. + + Tapping "Toggle counter state" will flip between the `nil` and non-`nil` counter states. + """ + +struct OptionalBasicsState: Equatable { + var optionalCounter: CounterState? +} + +enum OptionalBasicsAction: Equatable { + case optionalCounter(CounterAction) + case toggleCounterButtonTapped +} + +struct OptionalBasicsEnvironment {} + +let optionalBasicsReducer = + counterReducer + .optional() + .pullback( + state: \.optionalCounter, + action: /OptionalBasicsAction.optionalCounter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + OptionalBasicsState, OptionalBasicsAction, OptionalBasicsEnvironment + > { state, action, environment in + switch action { + case .toggleCounterButtonTapped: + state.optionalCounter = + state.optionalCounter == nil + ? CounterState() + : nil + return .none + case .optionalCounter: + return .none + } + } + ) + +struct OptionalBasicsView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + Button("Toggle counter state") { + viewStore.send(.toggleCounterButtonTapped) + } + + IfLetStore( + self.store.scope( + state: \.optionalCounter, + action: OptionalBasicsAction.optionalCounter + ), + then: { store in + Text(template: "`CounterState` is non-`nil`") + CounterView(store: store) + .buttonStyle(.borderless) + .frame(maxWidth: .infinity) + }, + else: { + Text(template: "`CounterState` is `nil`") + } + ) + } + } + .navigationTitle("Optional state") + } +} + +struct OptionalBasicsView_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationView { + OptionalBasicsView( + store: Store( + initialState: OptionalBasicsState(), + reducer: optionalBasicsReducer, + environment: OptionalBasicsEnvironment() + ) + ) + } + + NavigationView { + OptionalBasicsView( + store: Store( + initialState: OptionalBasicsState(optionalCounter: CounterState(count: 42)), + reducer: optionalBasicsReducer, + environment: OptionalBasicsEnvironment() + ) + ) + } + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift new file mode 100755 index 0000000..34b7f98 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/01-GettingStarted-SharedState.swift @@ -0,0 +1,278 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how multiple independent screens can share state in the Composable \ + Architecture. Each tab manages its own state, and could be in separate modules, but changes in \ + one tab are immediately reflected in the other. + + This tab has its own state, consisting of a count value that can be incremented and decremented, \ + as well as an alert value that is set when asking if the current count is prime. + + Internally, it is also keeping track of various stats, such as min and max counts and total \ + number of count events that occurred. Those states are viewable in the other tab, and the stats \ + can be reset from the other tab. + """ + +struct SharedState: Equatable { + var counter = CounterState() + var currentTab = Tab.counter + + enum Tab { case counter, profile } + + struct CounterState: Equatable { + var alert: AlertState? + var count = 0 + var maxCount = 0 + var minCount = 0 + var numberOfCounts = 0 + } + + // The ProfileState can be derived from the CounterState by getting and setting the parts it cares + // about. This allows the profile feature to operate on a subset of app state instead of the whole + // thing. + var profile: ProfileState { + get { + ProfileState( + currentTab: self.currentTab, + count: self.counter.count, + maxCount: self.counter.maxCount, + minCount: self.counter.minCount, + numberOfCounts: self.counter.numberOfCounts + ) + } + set { + self.currentTab = newValue.currentTab + self.counter.count = newValue.count + self.counter.maxCount = newValue.maxCount + self.counter.minCount = newValue.minCount + self.counter.numberOfCounts = newValue.numberOfCounts + } + } + + struct ProfileState: Equatable { + private(set) var currentTab: Tab + private(set) var count = 0 + private(set) var maxCount: Int + private(set) var minCount: Int + private(set) var numberOfCounts: Int + + fileprivate mutating func resetCount() { + self.currentTab = .counter + self.count = 0 + self.maxCount = 0 + self.minCount = 0 + self.numberOfCounts = 0 + } + } +} + +enum SharedStateAction: Equatable { + case counter(CounterAction) + case profile(ProfileAction) + case selectTab(SharedState.Tab) + + enum CounterAction: Equatable { + case alertDismissed + case decrementButtonTapped + case incrementButtonTapped + case isPrimeButtonTapped + } + + enum ProfileAction: Equatable { + case resetCounterButtonTapped + } +} + +let sharedStateCounterReducer = Reducer< + SharedState.CounterState, SharedStateAction.CounterAction, Void +> { state, action, _ in + switch action { + case .alertDismissed: + state.alert = nil + return .none + + case .decrementButtonTapped: + state.count -= 1 + state.numberOfCounts += 1 + state.minCount = min(state.minCount, state.count) + return .none + + case .incrementButtonTapped: + state.count += 1 + state.numberOfCounts += 1 + state.maxCount = max(state.maxCount, state.count) + return .none + + case .isPrimeButtonTapped: + state.alert = AlertState( + title: TextState( + isPrime(state.count) + ? "👍 The number \(state.count) is prime!" + : "👎 The number \(state.count) is not prime :(" + ) + ) + return .none + } +} + +let sharedStateProfileReducer = Reducer< + SharedState.ProfileState, SharedStateAction.ProfileAction, Void +> { state, action, _ in + switch action { + case .resetCounterButtonTapped: + state.resetCount() + return .none + } +} + +let sharedStateReducer = Reducer.combine( + sharedStateCounterReducer.pullback( + state: \SharedState.counter, + action: /SharedStateAction.counter, + environment: { _ in () } + ), + sharedStateProfileReducer.pullback( + state: \SharedState.profile, + action: /SharedStateAction.profile, + environment: { _ in () } + ), + Reducer { state, action, _ in + switch action { + case .counter, .profile: + return .none + case let .selectTab(tab): + state.currentTab = tab + return .none + } + } +) + +struct SharedStateView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: \.currentTab) { viewStore in + VStack { + Picker( + "Tab", + selection: viewStore.binding(send: SharedStateAction.selectTab) + ) { + Text("Counter") + .tag(SharedState.Tab.counter) + + Text("Profile") + .tag(SharedState.Tab.profile) + } + .pickerStyle(.segmented) + + if viewStore.state == .counter { + SharedStateCounterView( + store: self.store.scope(state: \.counter, action: SharedStateAction.counter)) + } + + if viewStore.state == .profile { + SharedStateProfileView( + store: self.store.scope(state: \.profile, action: SharedStateAction.profile)) + } + + Spacer() + } + } + .padding() + } +} + +struct SharedStateCounterView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(spacing: 64) { + Text(template: readMe, .caption) + + VStack(spacing: 16) { + HStack { + Button { + viewStore.send(.decrementButtonTapped) + } label: { + Image(systemName: "minus") + } + + Text("\(viewStore.count)") + .monospacedDigit() + + Button { + viewStore.send(.incrementButtonTapped) + } label: { + Image(systemName: "plus") + } + } + + Button("Is this prime?") { viewStore.send(.isPrimeButtonTapped) } + } + } + .padding(.top) + .navigationTitle("Shared State Demo") + .alert(self.store.scope(state: \.alert), dismiss: .alertDismissed) + } + } +} + +struct SharedStateProfileView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(spacing: 64) { + Text( + template: """ + This tab shows state from the previous tab, and it is capable of reseting all of the \ + state back to 0. + + This shows that it is possible for each screen to model its state in the way that makes \ + the most sense for it, while still allowing the state and mutations to be shared \ + across independent screens. + """, + .caption + ) + + VStack(spacing: 16) { + Text("Current count: \(viewStore.count)") + Text("Max count: \(viewStore.maxCount)") + Text("Min count: \(viewStore.minCount)") + Text("Total number of count events: \(viewStore.numberOfCounts)") + Button("Reset") { viewStore.send(.resetCounterButtonTapped) } + } + } + .padding(.top) + .navigationTitle("Profile") + } + } +} + +// MARK: - SwiftUI previews + +struct SharedState_Previews: PreviewProvider { + static var previews: some View { + SharedStateView( + store: Store( + initialState: SharedState(), + reducer: sharedStateReducer, + environment: () + ) + ) + } +} + +// MARK: - Private helpers + +/// Checks if a number is prime or not. +private func isPrime(_ p: Int) -> Bool { + if p <= 1 { return false } + if p <= 3 { return true } + for i in 2...Int(sqrtf(Float(p))) { + if p % i == 0 { return false } + } + return true +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift new file mode 100755 index 0000000..d01fe2f --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Basics.swift @@ -0,0 +1,177 @@ +import Combine +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how to introduce side effects into a feature built with the \ + Composable Architecture. + + A side effect is a unit of work that needs to be performed in the outside world. For example, an \ + API request needs to reach an external service over HTTP, which brings with it lots of \ + uncertainty and complexity. + + Many things we do in our applications involve side effects, such as timers, database requests, \ + file access, socket connections, and anytime a scheduler is involved (such as debouncing, \ + throttling and delaying), and they are typically difficult to test. + + This application has a simple side effect: tapping "Number fact" will trigger an API request to \ + load a piece of trivia about that number. This effect is handled by the reducer, and a full test \ + suite is written to confirm that the effect behaves in the way we expect. + """ + +// MARK: - Feature domain + +struct EffectsBasicsState: Equatable { + var count = 0 + var isNumberFactRequestInFlight = false + var numberFact: String? +} + +enum EffectsBasicsAction: Equatable { + case decrementButtonTapped + case decrementDelayResponse + case incrementButtonTapped + case numberFactButtonTapped + case numberFactResponse(TaskResult) +} + +struct EffectsBasicsEnvironment { + var fact: FactClient + var mainQueue: AnySchedulerOf +} + +// MARK: - Feature business logic + +let effectsBasicsReducer = Reducer< + EffectsBasicsState, + EffectsBasicsAction, + EffectsBasicsEnvironment +> { state, action, environment in + enum DelayID {} + + switch action { + case .decrementButtonTapped: + state.count -= 1 + state.numberFact = nil + // Return an effect that re-increments the count after 1 second if the count is negative + return state.count >= 0 + ? .none + : .task { + try await environment.mainQueue.sleep(for: 1) + return .decrementDelayResponse + } + .cancellable(id: DelayID.self) + + case .decrementDelayResponse: + if state.count < 0 { + state.count += 1 + } + return .none + + case .incrementButtonTapped: + state.count += 1 + state.numberFact = nil + return state.count >= 0 + ? .cancel(id: DelayID.self) + : .none + + case .numberFactButtonTapped: + state.isNumberFactRequestInFlight = true + state.numberFact = nil + // Return an effect that fetches a number fact from the API and returns the + // value back to the reducer's `numberFactResponse` action. + return .task { [count = state.count] in + await .numberFactResponse(TaskResult { try await environment.fact.fetch(count) }) + } + + case let .numberFactResponse(.success(response)): + state.isNumberFactRequestInFlight = false + state.numberFact = response + return .none + + case .numberFactResponse(.failure): + // NB: This is where we could handle the error is some way, such as showing an alert. + state.isNumberFactRequestInFlight = false + return .none + } +} + +// MARK: - Feature view + +struct EffectsBasicsView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + Section { + HStack { + Button { + viewStore.send(.decrementButtonTapped) + } label: { + Image(systemName: "minus") + } + + Text("\(viewStore.count)") + .monospacedDigit() + + Button { + viewStore.send(.incrementButtonTapped) + } label: { + Image(systemName: "plus") + } + } + .frame(maxWidth: .infinity) + + Button("Number fact") { viewStore.send(.numberFactButtonTapped) } + .frame(maxWidth: .infinity) + + if viewStore.isNumberFactRequestInFlight { + ProgressView() + .frame(maxWidth: .infinity) + // NB: There seems to be a bug in SwiftUI where the progress view does not show + // a second time unless it is given a new identity. + .id(UUID()) + } + + if let numberFact = viewStore.numberFact { + Text(numberFact) + } + } + + Section { + Button("Number facts provided by numbersapi.com") { + UIApplication.shared.open(URL(string: "http://numbersapi.com")!) + } + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + } + } + .buttonStyle(.borderless) + } + .navigationTitle("Effects") + } +} + +// MARK: - Feature SwiftUI previews + +struct EffectsBasicsView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + EffectsBasicsView( + store: Store( + initialState: EffectsBasicsState(), + reducer: effectsBasicsReducer, + environment: EffectsBasicsEnvironment( + fact: .live, + mainQueue: .main + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift new file mode 100755 index 0000000..c38c4af --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Cancellation.swift @@ -0,0 +1,141 @@ +import Combine +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how one can cancel in-flight effects in the Composable Architecture. + + Use the stepper to count to a number, and then tap the "Number fact" button to fetch \ + a random fact about that number using an API. + + While the API request is in-flight, you can tap "Cancel" to cancel the effect and prevent \ + it from feeding data back into the application. Interacting with the stepper while a \ + request is in-flight will also cancel it. + """ + +// MARK: - Demo app domain + +struct EffectsCancellationState: Equatable { + var count = 0 + var currentFact: String? + var isFactRequestInFlight = false +} + +enum EffectsCancellationAction: Equatable { + case cancelButtonTapped + case stepperChanged(Int) + case factButtonTapped + case factResponse(TaskResult) +} + +struct EffectsCancellationEnvironment { + var fact: FactClient +} + +// MARK: - Business logic + +let effectsCancellationReducer = Reducer< + EffectsCancellationState, EffectsCancellationAction, EffectsCancellationEnvironment +> { state, action, environment in + + enum NumberFactRequestID {} + + switch action { + case .cancelButtonTapped: + state.isFactRequestInFlight = false + return .cancel(id: NumberFactRequestID.self) + + case let .stepperChanged(value): + state.count = value + state.currentFact = nil + state.isFactRequestInFlight = false + return .cancel(id: NumberFactRequestID.self) + + case .factButtonTapped: + state.currentFact = nil + state.isFactRequestInFlight = true + + return .task { [count = state.count] in + await .factResponse(TaskResult { try await environment.fact.fetch(count) }) + } + .cancellable(id: NumberFactRequestID.self) + + case let .factResponse(.success(response)): + state.isFactRequestInFlight = false + state.currentFact = response + return .none + + case .factResponse(.failure): + state.isFactRequestInFlight = false + return .none + } +} + +// MARK: - Application view + +struct EffectsCancellationView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + Section { + Stepper( + "\(viewStore.count)", + value: viewStore.binding(get: \.count, send: EffectsCancellationAction.stepperChanged) + ) + + if viewStore.isFactRequestInFlight { + HStack { + Button("Cancel") { viewStore.send(.cancelButtonTapped) } + Spacer() + ProgressView() + // NB: There seems to be a bug in SwiftUI where the progress view does not show + // a second time unless it is given a new identity. + .id(UUID()) + } + } else { + Button("Number fact") { viewStore.send(.factButtonTapped) } + .disabled(viewStore.isFactRequestInFlight) + } + + viewStore.currentFact.map { + Text($0).padding(.vertical, 8) + } + } + + Section { + Button("Number facts provided by numbersapi.com") { + UIApplication.shared.open(URL(string: "http://numbersapi.com")!) + } + .foregroundStyle(.secondary) + .frame(maxWidth: .infinity) + } + } + .buttonStyle(.borderless) + } + .navigationTitle("Effect cancellation") + } +} + +// MARK: - SwiftUI previews + +struct EffectsCancellation_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + EffectsCancellationView( + store: Store( + initialState: EffectsCancellationState(), + reducer: effectsCancellationReducer, + environment: EffectsCancellationEnvironment( + fact: .live + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-LongLiving.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-LongLiving.swift new file mode 100755 index 0000000..e6fa4a0 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-LongLiving.swift @@ -0,0 +1,110 @@ +import Combine +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This application demonstrates how to handle long-living effects, for example notifications from \ + Notification Center, and how to tie an effect's lifetime to the lifetime of the view. + + Run this application in the simulator, and take a few screenshots by going to \ + *Device › Screenshot* in the menu, and observe that the UI counts the number of times that \ + happens. + + Then, navigate to another screen and take screenshots there, and observe that this screen does \ + *not* count those screenshots. The notifications effect is automatically cancelled when leaving \ + the screen, and restarted when entering the screen. + """ + +// MARK: - Application domain + +struct LongLivingEffectsState: Equatable { + var screenshotCount = 0 +} + +enum LongLivingEffectsAction { + case task + case userDidTakeScreenshotNotification +} + +struct LongLivingEffectsEnvironment { + var screenshots: @Sendable () async -> AsyncStream +} + +// MARK: - Business logic + +let longLivingEffectsReducer = Reducer< + LongLivingEffectsState, LongLivingEffectsAction, LongLivingEffectsEnvironment +> { state, action, environment in + switch action { + case .task: + // When the view appears, start the effect that emits when screenshots are taken. + return .run { send in + for await _ in await environment.screenshots() { + await send(.userDidTakeScreenshotNotification) + } + } + + case .userDidTakeScreenshotNotification: + state.screenshotCount += 1 + return .none + } +} + +// MARK: - SwiftUI view + +struct LongLivingEffectsView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + Text("A screenshot of this screen has been taken \(viewStore.screenshotCount) times.") + .font(.headline) + + Section { + NavigationLink(destination: self.detailView) { + Text("Navigate to another screen") + } + } + } + .navigationTitle("Long-living effects") + .task { await viewStore.send(.task).finish() } + } + } + + var detailView: some View { + Text( + """ + Take a screenshot of this screen a few times, and then go back to the previous screen to see \ + that those screenshots were not counted. + """ + ) + .padding(.horizontal, 64) + .navigationBarTitleDisplayMode(.inline) + } +} + +// MARK: - SwiftUI previews + +struct EffectsLongLiving_Previews: PreviewProvider { + static var previews: some View { + let appView = LongLivingEffectsView( + store: Store( + initialState: LongLivingEffectsState(), + reducer: longLivingEffectsReducer, + environment: LongLivingEffectsEnvironment( + screenshots: { .init { _ in } } + ) + ) + ) + + return Group { + NavigationView { appView } + NavigationView { appView.detailView } + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Refreshable.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Refreshable.swift new file mode 100755 index 0000000..e5b9c2b --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Refreshable.swift @@ -0,0 +1,132 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This application demonstrates how to make use of SwiftUI's `refreshable` API in the Composable \ + Architecture. Use the "-" and "+" buttons to count up and down, and then pull down to request \ + a fact about that number. + + There is an overload of the `.send` method that allows you to suspend and await while a piece \ + of state is true. You can use this method to communicate to SwiftUI that you are \ + currently fetching data so that it knows to continue showing the loading indicator. + """ + +struct RefreshableState: Equatable { + var count = 0 + var fact: String? +} + +enum RefreshableAction: Equatable { + case cancelButtonTapped + case decrementButtonTapped + case factResponse(TaskResult) + case incrementButtonTapped + case refresh +} + +struct RefreshableEnvironment { + var fact: FactClient + var mainQueue: AnySchedulerOf +} + +let refreshableReducer = Reducer< + RefreshableState, + RefreshableAction, + RefreshableEnvironment +> { state, action, environment in + + enum FactRequestID {} + + switch action { + case .cancelButtonTapped: + return .cancel(id: FactRequestID.self) + + case .decrementButtonTapped: + state.count -= 1 + return .none + + case let .factResponse(.success(fact)): + state.fact = fact + return .none + + case .factResponse(.failure): + // NB: This is where you could do some error handling. + return .none + + case .incrementButtonTapped: + state.count += 1 + return .none + + case .refresh: + state.fact = nil + return .task { [count = state.count] in + await .factResponse(TaskResult { try await environment.fact.fetch(count) }) + } + .animation() + .cancellable(id: FactRequestID.self) + } +} + +struct RefreshableView: View { + @State var isLoading = false + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + List { + Section { + AboutView(readMe: readMe) + } + + HStack { + Button { + viewStore.send(.decrementButtonTapped) + } label: { + Image(systemName: "minus") + } + + Text("\(viewStore.count)") + .monospacedDigit() + + Button { + viewStore.send(.incrementButtonTapped) + } label: { + Image(systemName: "plus") + } + } + .frame(maxWidth: .infinity) + .buttonStyle(.borderless) + + if let fact = viewStore.fact { + Text(fact) + .bold() + } + if self.isLoading { + Button("Cancel") { + viewStore.send(.cancelButtonTapped, animation: .default) + } + } + } + .refreshable { + self.isLoading = true + defer { self.isLoading = false } + await viewStore.send(.refresh).finish() + } + } + } +} + +struct Refreshable_Previews: PreviewProvider { + static var previews: some View { + RefreshableView( + store: Store( + initialState: RefreshableState(), + reducer: refreshableReducer, + environment: RefreshableEnvironment( + fact: .live, + mainQueue: .main + ) + ) + ) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-SystemEnvironment.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-SystemEnvironment.swift new file mode 100755 index 0000000..294498b --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-SystemEnvironment.swift @@ -0,0 +1,243 @@ +import ComposableArchitecture +import Foundation +import SwiftUI + +private let readMe = """ + This screen demonstrates how one can share system-wide dependencies across many features with \ + very little work. The idea is to create a `SystemEnvironment` generic type that wraps an \ + environment, and then implement dynamic member lookup so that you can seamlessly use the \ + dependencies in both environments. + + Then, throughout your application you can wrap your environments in the `SystemEnvironment` \ + to get instant access to all of the shared dependencies. Some good candidates for dependencies \ + to share are things like date initializers, schedulers (especially `DispatchQueue.main`), `UUID` \ + initializers, and any other dependency in your application that you want every reducer to have \ + access to. + """ + +struct MultipleDependenciesState: Equatable { + var alert: AlertState? + var dateString: String? + var fetchedNumberString: String? + var isFetchInFlight = false + var uuidString: String? +} + +enum MultipleDependenciesAction: Equatable { + case alertButtonTapped + case alertDelayReceived + case alertDismissed + case dateButtonTapped + case fetchNumberButtonTapped + case fetchNumberResponse(Int) + case uuidButtonTapped +} + +struct MultipleDependenciesEnvironment { + var fetchNumber: @Sendable () async throws -> Int +} + +let multipleDependenciesReducer = Reducer< + MultipleDependenciesState, + MultipleDependenciesAction, + SystemEnvironment +> { state, action, environment in + + switch action { + case .alertButtonTapped: + return .task { + try await environment.mainQueue.sleep(for: 1) + return .alertDelayReceived + } + + case .alertDelayReceived: + state.alert = AlertState(title: TextState("Here's an alert after a delay!")) + return .none + + case .alertDismissed: + state.alert = nil + return .none + + case .dateButtonTapped: + state.dateString = "\(environment.date())" + return .none + + case .fetchNumberButtonTapped: + state.isFetchInFlight = true + return .task { .fetchNumberResponse(try await environment.fetchNumber()) } + + case let .fetchNumberResponse(number): + state.isFetchInFlight = false + state.fetchedNumberString = "\(number)" + return .none + + case .uuidButtonTapped: + state.uuidString = "\(environment.uuid())" + return .none + } +} + +struct MultipleDependenciesView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + AboutView(readMe: readMe) + + Section { + HStack { + Button("Date") { viewStore.send(.dateButtonTapped) } + if let dateString = viewStore.dateString { + Spacer() + Text(dateString) + } + } + + HStack { + Button("UUID") { viewStore.send(.uuidButtonTapped) } + if let uuidString = viewStore.uuidString { + Spacer() + Text(uuidString) + .minimumScaleFactor(0.5) + .lineLimit(1) + } + } + + Button("Delayed Alert") { viewStore.send(.alertButtonTapped) } + .alert(self.store.scope(state: \.alert), dismiss: .alertDismissed) + } header: { + Text( + template: """ + The actions below make use of the dependencies in the `SystemEnvironment`. + """, .caption + ) + .textCase(.none) + } + + Section { + HStack { + Button("Fetch Number") { viewStore.send(.fetchNumberButtonTapped) } + .disabled(viewStore.isFetchInFlight) + Spacer() + + if viewStore.isFetchInFlight { + ProgressView() + } else if let fetchedNumberString = viewStore.fetchedNumberString { + Text(fetchedNumberString) + } + } + } header: { + Text( + template: """ + The actions below make use of the custom environment for this screen, which holds a \ + dependency for fetching a random number. + """, .caption + ) + .textCase(.none) + } + + } + .buttonStyle(.borderless) + } + .navigationTitle("System Environment") + } +} + +struct MultipleDependenciesView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + MultipleDependenciesView( + store: Store( + initialState: MultipleDependenciesState(), + reducer: multipleDependenciesReducer, + environment: .live( + environment: MultipleDependenciesEnvironment( + fetchNumber: { + try await Task.sleep(nanoseconds: NSEC_PER_SEC) + return Int.random(in: 1...1_000) + } + ) + ) + ) + ) + } + } +} + +@dynamicMemberLookup +struct SystemEnvironment { + var date: @Sendable () -> Date + var environment: Environment + var mainQueue: AnySchedulerOf + var uuid: @Sendable () -> UUID + + subscript( + dynamicMember keyPath: WritableKeyPath + ) -> Dependency { + get { self.environment[keyPath: keyPath] } + set { self.environment[keyPath: keyPath] = newValue } + } + + /// Creates a live system environment with the wrapped environment provided. + /// + /// - Parameter environment: An environment to be wrapped in the system environment. + /// - Returns: A new system environment. + static func live(environment: Environment) -> Self { + Self( + date: { Date() }, + environment: environment, + mainQueue: .main, + uuid: { UUID() } + ) + } + + /// Transforms the underlying wrapped environment. + func map( + _ transform: @escaping (Environment) -> NewEnvironment + ) -> SystemEnvironment { + .init( + date: self.date, + environment: transform(self.environment), + mainQueue: self.mainQueue, + uuid: self.uuid + ) + } +} + +extension SystemEnvironment: Sendable where Environment: Sendable {} + +#if DEBUG + import XCTestDynamicOverlay + + extension SystemEnvironment { + static func unimplemented( + date: @escaping @Sendable () -> Date = XCTUnimplemented( + "\(Self.self).date", placeholder: Date() + ), + environment: Environment, + mainQueue: AnySchedulerOf = .unimplemented, + uuid: @escaping @Sendable () -> UUID = XCTUnimplemented( + "\(Self.self).uuid", placeholder: UUID() + ) + ) -> Self { + Self( + date: date, + environment: environment, + mainQueue: mainQueue, + uuid: uuid + ) + } + } +#endif + +extension UUID { + /// A deterministic, auto-incrementing "UUID" generator for testing. + static var incrementing: () -> UUID { + var uuid = 0 + return { + defer { uuid += 1 } + return UUID(uuidString: "00000000-0000-0000-0000-\(String(format: "%012x", uuid))")! + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Timers.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Timers.swift new file mode 100755 index 0000000..f86cd1c --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-Timers.swift @@ -0,0 +1,130 @@ +import Combine +import ComposableArchitecture +@preconcurrency import SwiftUI // NB: SwiftUI.Animation is not Sendable yet. + +private let readMe = """ + This application demonstrates how to work with timers in the Composable Architecture. + + It makes use of the `.timer` method on Combine Schedulers, which is a helper provided by the \ + Combine Schedulers library included with this library. The helper provides an \ + `AsyncSequence`-friendly API for dealing with timers in asynchronous code. + """ + +// MARK: - Timer feature domain + +struct TimersState: Equatable { + var isTimerActive = false + var secondsElapsed = 0 +} + +enum TimersAction { + case timerTicked + case toggleTimerButtonTapped +} + +struct TimersEnvironment { + var mainQueue: AnySchedulerOf +} + +let timersReducer = Reducer { + state, action, environment in + + enum TimerID {} + + switch action { + case .timerTicked: + state.secondsElapsed += 1 + return .none + + case .toggleTimerButtonTapped: + state.isTimerActive.toggle() + return .run { [isTimerActive = state.isTimerActive] send in + guard isTimerActive else { return } + for await _ in environment.mainQueue.timer(interval: 1) { + await send(.timerTicked, animation: .interpolatingSpring(stiffness: 3000, damping: 40)) + } + } + .cancellable(id: TimerID.self, cancelInFlight: true) + } +} + +// MARK: - Timer feature view + +struct TimersView: View { + let store: Store + + var body: some View { + WithViewStore(store) { viewStore in + Form { + AboutView(readMe: readMe) + + ZStack { + Circle() + .fill( + AngularGradient( + gradient: Gradient( + colors: [ + .blue.opacity(0.3), + .blue, + .blue, + .green, + .green, + .yellow, + .yellow, + .red, + .red, + .purple, + .purple, + .purple.opacity(0.3), + ] + ), + center: .center + ) + ) + .rotationEffect(.degrees(-90)) + GeometryReader { proxy in + Path { path in + path.move(to: CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)) + path.addLine(to: CGPoint(x: proxy.size.width / 2, y: 0)) + } + .stroke(.primary, lineWidth: 3) + .rotationEffect(.degrees(Double(viewStore.secondsElapsed) * 360 / 60)) + } + } + .aspectRatio(1, contentMode: .fit) + .frame(maxWidth: 280) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + + Button { + viewStore.send(.toggleTimerButtonTapped) + } label: { + Text(viewStore.isTimerActive ? "Stop" : "Start") + .padding(8) + } + .frame(maxWidth: .infinity) + .tint(viewStore.isTimerActive ? Color.red : .accentColor) + .buttonStyle(.borderedProminent) + } + .navigationTitle("Timers") + } + } +} + +// MARK: - SwiftUI previews + +struct TimersView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + TimersView( + store: Store( + initialState: TimersState(), + reducer: timersReducer, + environment: TimersEnvironment( + mainQueue: .main + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift new file mode 100755 index 0000000..61ab6c9 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/02-Effects-WebSocket.swift @@ -0,0 +1,341 @@ +import Combine +import ComposableArchitecture +import SwiftUI +import XCTestDynamicOverlay + +private let readMe = """ + This application demonstrates how to work with a web socket in the Composable Architecture. + + A lightweight wrapper is made for `URLSession`'s API for web sockets so that we can send, \ + receive and ping a socket endpoint. To test, connect to the socket server, and then send a \ + message. The socket server should immediately reply with the exact message you send it. + """ + +struct WebSocketState: Equatable { + var alert: AlertState? + var connectivityState = ConnectivityState.disconnected + var messageToSend = "" + var receivedMessages: [String] = [] + + enum ConnectivityState: String { + case connected + case connecting + case disconnected + } +} + +enum WebSocketAction: Equatable { + case alertDismissed + case connectButtonTapped + case messageToSendChanged(String) + case receivedSocketMessage(TaskResult) + case sendButtonTapped + case sendResponse(didSucceed: Bool) + case webSocket(WebSocketClient.Action) +} + +struct WebSocketEnvironment { + var mainQueue: AnySchedulerOf + var webSocket: WebSocketClient +} + +let webSocketReducer = Reducer { + state, action, environment in + + enum WebSocketID {} + + switch action { + case .alertDismissed: + state.alert = nil + return .none + + case .connectButtonTapped: + switch state.connectivityState { + case .connected, .connecting: + state.connectivityState = .disconnected + return .cancel(id: WebSocketID.self) + + case .disconnected: + state.connectivityState = .connecting + return .run { send in + let actions = await environment.webSocket + .open(WebSocketID.self, URL(string: "wss://echo.websocket.events")!, []) + await withThrowingTaskGroup(of: Void.self) { group in + for await action in actions { + await send(.webSocket(action)) + switch action { + case .didOpen: + group.addTask { + while true { + try await environment.mainQueue.sleep(for: .seconds(10)) + try await environment.webSocket.sendPing(WebSocketID.self) + } + } + group.addTask { + for await result in try await environment.webSocket.receive(WebSocketID.self) { + await send(.receivedSocketMessage(result)) + } + } + case .didClose: + return + } + } + } + } catch: { _, _ in + } + .cancellable(id: WebSocketID.self) + } + + case let .messageToSendChanged(message): + state.messageToSend = message + return .none + + case let .receivedSocketMessage(.success(message)): + if case let .string(string) = message { + state.receivedMessages.append(string) + } + return .none + + case .receivedSocketMessage(.failure): + return .none + + case .sendButtonTapped: + let messageToSend = state.messageToSend + state.messageToSend = "" + return .task { + try await environment.webSocket.send(WebSocketID.self, .string(messageToSend)) + return .sendResponse(didSucceed: true) + } catch: { _ in + .sendResponse(didSucceed: false) + } + .cancellable(id: WebSocketID.self) + + case .sendResponse(didSucceed: false): + state.alert = AlertState(title: TextState("Could not send socket message. Try again.")) + return .none + + case .sendResponse(didSucceed: true): + return .none + + case .webSocket(.didClose): + state.connectivityState = .disconnected + return .cancel(id: WebSocketID.self) + + case .webSocket(.didOpen): + state.connectivityState = .connected + return .none + } +} + +struct WebSocketView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(alignment: .leading) { + AboutView(readMe: readMe) + .padding(.bottom) + + HStack { + TextField( + "Message to send", + text: viewStore.binding( + get: \.messageToSend, send: WebSocketAction.messageToSendChanged) + ) + + Button( + viewStore.connectivityState == .connected + ? "Disconnect" + : viewStore.connectivityState == .disconnected + ? "Connect" + : "Connecting..." + ) { + viewStore.send(.connectButtonTapped) + } + } + + Button("Send message") { + viewStore.send(.sendButtonTapped) + } + + Spacer() + + Text("Status: \(viewStore.connectivityState.rawValue)") + .foregroundStyle(.secondary) + Text("Received messages:") + .foregroundStyle(.secondary) + Text(viewStore.receivedMessages.joined(separator: "\n")) + } + .padding() + .alert(self.store.scope(state: \.alert), dismiss: .alertDismissed) + .navigationTitle("Web Socket") + } + } +} + +// MARK: - WebSocketClient + +struct WebSocketClient { + enum Action: Equatable { + case didOpen(protocol: String?) + case didClose(code: URLSessionWebSocketTask.CloseCode, reason: Data?) + } + + enum Message: Equatable { + struct Unknown: Error {} + + case data(Data) + case string(String) + + init(_ message: URLSessionWebSocketTask.Message) throws { + switch message { + case let .data(data): self = .data(data) + case let .string(string): self = .string(string) + @unknown default: throw Unknown() + } + } + } + + var open: @Sendable (Any.Type, URL, [String]) async -> AsyncStream + var receive: @Sendable (Any.Type) async throws -> AsyncStream> + var send: @Sendable (Any.Type, URLSessionWebSocketTask.Message) async throws -> Void + var sendPing: @Sendable (Any.Type) async throws -> Void +} + +extension WebSocketClient { + static var live: Self { + final actor WebSocketActor: GlobalActor { + final class Delegate: NSObject, URLSessionWebSocketDelegate { + var continuation: AsyncStream.Continuation? + + func urlSession( + _: URLSession, + webSocketTask _: URLSessionWebSocketTask, + didOpenWithProtocol protocol: String? + ) { + self.continuation?.yield(.didOpen(protocol: `protocol`)) + } + + func urlSession( + _: URLSession, + webSocketTask _: URLSessionWebSocketTask, + didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, + reason: Data? + ) { + self.continuation?.yield(.didClose(code: closeCode, reason: reason)) + self.continuation?.finish() + } + } + + typealias Dependencies = (socket: URLSessionWebSocketTask, delegate: Delegate) + + static let shared = WebSocketActor() + + var dependencies: [ObjectIdentifier: Dependencies] = [:] + + func open(id: Any.Type, url: URL, protocols: [String]) -> AsyncStream { + let id = ObjectIdentifier(id) + let delegate = Delegate() + let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil) + let socket = session.webSocketTask(with: url, protocols: protocols) + defer { socket.resume() } + var continuation: AsyncStream.Continuation! + let stream = AsyncStream { + $0.onTermination = { _ in + socket.cancel() + Task { await self.removeDependencies(id: id) } + } + continuation = $0 + } + delegate.continuation = continuation + self.dependencies[id] = (socket, delegate) + return stream + } + + func close( + id: Any.Type, with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data? + ) async throws { + let id = ObjectIdentifier(id) + defer { self.dependencies[id] = nil } + try self.socket(id: id).cancel(with: closeCode, reason: reason) + } + + func receive(id: Any.Type) throws -> AsyncStream> { + let socket = try self.socket(id: ObjectIdentifier(id)) + return AsyncStream { continuation in + let task = Task { + while !Task.isCancelled { + continuation.yield(await TaskResult { try await Message(socket.receive()) }) + } + continuation.finish() + } + continuation.onTermination = { _ in task.cancel() } + } + } + + func send(id: Any.Type, message: URLSessionWebSocketTask.Message) async throws { + try await self.socket(id: ObjectIdentifier(id)).send(message) + } + + func sendPing(id: Any.Type) async throws { + let socket = try self.socket(id: ObjectIdentifier(id)) + return try await withCheckedThrowingContinuation { continuation in + socket.sendPing { error in + if let error = error { + continuation.resume(throwing: error) + } else { + continuation.resume() + } + } + } + } + + private func socket(id: ObjectIdentifier) throws -> URLSessionWebSocketTask { + guard let dependencies = self.dependencies[id]?.socket else { + struct Closed: Error {} + throw Closed() + } + return dependencies + } + + private func removeDependencies(id: ObjectIdentifier) { + self.dependencies[id] = nil + } + } + + return Self( + open: { await WebSocketActor.shared.open(id: $0, url: $1, protocols: $2) }, + receive: { try await WebSocketActor.shared.receive(id: $0) }, + send: { try await WebSocketActor.shared.send(id: $0, message: $1) }, + sendPing: { try await WebSocketActor.shared.sendPing(id: $0) } + ) + } +} + +extension WebSocketClient { + static let unimplemented = Self( + open: XCTUnimplemented("\(Self.self).open", placeholder: AsyncStream.never), + receive: XCTUnimplemented("\(Self.self).receive"), + send: XCTUnimplemented("\(Self.self).send"), + sendPing: XCTUnimplemented("\(Self.self).sendPing") + ) +} + +// MARK: - SwiftUI previews + +struct WebSocketView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + WebSocketView( + store: Store( + initialState: WebSocketState(receivedMessages: ["Echo"]), + reducer: webSocketReducer, + environment: WebSocketEnvironment( + mainQueue: .main, + webSocket: .live + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-LoadThenNavigate.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-LoadThenNavigate.swift new file mode 100755 index 0000000..a1eeeb7 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-LoadThenNavigate.swift @@ -0,0 +1,156 @@ +import ComposableArchitecture +import Foundation +import SwiftUI + +private let readMe = """ + This screen demonstrates navigation that depends on loading optional state from a list element. + + Tapping a row fires off an effect that will load its associated counter state a second later. \ + When the counter state is present, you will be programmatically navigated to the screen that \ + depends on this data. + """ + +struct LoadThenNavigateListState: Equatable { + var rows: IdentifiedArrayOf = [ + Row(count: 1, id: UUID()), + Row(count: 42, id: UUID()), + Row(count: 100, id: UUID()), + ] + var selection: Identified? + + struct Row: Equatable, Identifiable { + var count: Int + let id: UUID + var isActivityIndicatorVisible = false + } +} + +enum LoadThenNavigateListAction: Equatable { + case counter(CounterAction) + case onDisappear + case setNavigation(selection: UUID?) + case setNavigationSelectionDelayCompleted(UUID) +} + +struct LoadThenNavigateListEnvironment { + var mainQueue: AnySchedulerOf +} + +let loadThenNavigateListReducer = + counterReducer + .pullback( + state: \Identified.value, + action: .self, + environment: { $0 } + ) + .optional() + .pullback( + state: \LoadThenNavigateListState.selection, + action: /LoadThenNavigateListAction.counter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + LoadThenNavigateListState, LoadThenNavigateListAction, LoadThenNavigateListEnvironment + > { state, action, environment in + + enum CancelID {} + + switch action { + case .counter: + return .none + + case .onDisappear: + return .cancel(id: CancelID.self) + + case let .setNavigation(selection: .some(navigatedId)): + for row in state.rows { + state.rows[id: row.id]?.isActivityIndicatorVisible = row.id == navigatedId + } + return .task { + try await environment.mainQueue.sleep(for: 1) + return .setNavigationSelectionDelayCompleted(navigatedId) + } + .cancellable(id: CancelID.self, cancelInFlight: true) + + case .setNavigation(selection: .none): + if let selection = state.selection { + state.rows[id: selection.id]?.count = selection.count + } + state.selection = nil + return .cancel(id: CancelID.self) + + case let .setNavigationSelectionDelayCompleted(id): + state.rows[id: id]?.isActivityIndicatorVisible = false + state.selection = Identified( + CounterState(count: state.rows[id: id]?.count ?? 0), + id: id + ) + return .none + } + } + ) + +struct LoadThenNavigateListView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + ForEach(viewStore.rows) { row in + NavigationLink( + destination: IfLetStore( + self.store.scope( + state: \.selection?.value, + action: LoadThenNavigateListAction.counter + ) + ) { + CounterView(store: $0) + }, + tag: row.id, + selection: viewStore.binding( + get: \.selection?.id, + send: LoadThenNavigateListAction.setNavigation(selection:) + ) + ) { + HStack { + Text("Load optional counter that starts from \(row.count)") + if row.isActivityIndicatorVisible { + Spacer() + ProgressView() + } + } + } + } + } + .navigationTitle("Load then navigate") + .onDisappear { viewStore.send(.onDisappear) } + } + } +} + +struct LoadThenNavigateListView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + LoadThenNavigateListView( + store: Store( + initialState: LoadThenNavigateListState( + rows: [ + LoadThenNavigateListState.Row(count: 1, id: UUID()), + LoadThenNavigateListState.Row(count: 42, id: UUID()), + LoadThenNavigateListState.Row(count: 100, id: UUID()), + ] + ), + reducer: loadThenNavigateListReducer, + environment: LoadThenNavigateListEnvironment( + mainQueue: .main + ) + ) + ) + } + .navigationViewStyle(.stack) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-NavigateAndLoad.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-NavigateAndLoad.swift new file mode 100755 index 0000000..49d3ec4 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Lists-NavigateAndLoad.swift @@ -0,0 +1,136 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates navigation that depends on loading optional state from a list element. + + Tapping a row simultaneously navigates to a screen that depends on its associated counter state \ + and fires off an effect that will load this state a second later. + """ + +struct NavigateAndLoadListState: Equatable { + var rows: IdentifiedArrayOf = [ + Row(count: 1, id: UUID()), + Row(count: 42, id: UUID()), + Row(count: 100, id: UUID()), + ] + var selection: Identified? + + struct Row: Equatable, Identifiable { + var count: Int + let id: UUID + } +} + +enum NavigateAndLoadListAction: Equatable { + case counter(CounterAction) + case setNavigation(selection: UUID?) + case setNavigationSelectionDelayCompleted +} + +struct NavigateAndLoadListEnvironment { + var mainQueue: AnySchedulerOf +} + +let navigateAndLoadListReducer = + counterReducer + .optional() + .pullback(state: \Identified.value, action: .self, environment: { $0 }) + .optional() + .pullback( + state: \NavigateAndLoadListState.selection, + action: /NavigateAndLoadListAction.counter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + NavigateAndLoadListState, NavigateAndLoadListAction, NavigateAndLoadListEnvironment + > { state, action, environment in + + enum CancelID {} + + switch action { + case .counter: + return .none + + case let .setNavigation(selection: .some(id)): + state.selection = Identified(nil, id: id) + return .task { + try await environment.mainQueue.sleep(for: 1) + return .setNavigationSelectionDelayCompleted + } + .cancellable(id: CancelID.self, cancelInFlight: true) + + case .setNavigation(selection: .none): + if let selection = state.selection, let count = selection.value?.count { + state.rows[id: selection.id]?.count = count + } + state.selection = nil + return .cancel(id: CancelID.self) + + case .setNavigationSelectionDelayCompleted: + guard let id = state.selection?.id else { return .none } + state.selection?.value = CounterState(count: state.rows[id: id]?.count ?? 0) + return .none + } + } + ) + +struct NavigateAndLoadListView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + ForEach(viewStore.rows) { row in + NavigationLink( + destination: IfLetStore( + self.store.scope( + state: \.selection?.value, + action: NavigateAndLoadListAction.counter + ) + ) { + CounterView(store: $0) + } else: { + ProgressView() + }, + tag: row.id, + selection: viewStore.binding( + get: \.selection?.id, + send: NavigateAndLoadListAction.setNavigation(selection:) + ) + ) { + Text("Load optional counter that starts from \(row.count)") + } + } + } + } + .navigationTitle("Navigate and load") + } +} + +struct NavigateAndLoadListView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + NavigateAndLoadListView( + store: Store( + initialState: NavigateAndLoadListState( + rows: [ + NavigateAndLoadListState.Row(count: 1, id: UUID()), + NavigateAndLoadListState.Row(count: 42, id: UUID()), + NavigateAndLoadListState.Row(count: 100, id: UUID()), + ] + ), + reducer: navigateAndLoadListReducer, + environment: NavigateAndLoadListEnvironment( + mainQueue: .main + ) + ) + ) + } + .navigationViewStyle(.stack) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-LoadThenNavigate.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-LoadThenNavigate.swift new file mode 100755 index 0000000..b2f0326 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-LoadThenNavigate.swift @@ -0,0 +1,125 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates navigation that depends on loading optional state. + + Tapping "Load optional counter" fires off an effect that will load the counter state a second \ + later. When the counter state is present, you will be programmatically navigated to the screen \ + that depends on this data. + """ + +struct LoadThenNavigateState: Equatable { + var optionalCounter: CounterState? + var isActivityIndicatorVisible = false + + var isNavigationActive: Bool { self.optionalCounter != nil } +} + +enum LoadThenNavigateAction: Equatable { + case onDisappear + case optionalCounter(CounterAction) + case setNavigation(isActive: Bool) + case setNavigationIsActiveDelayCompleted +} + +struct LoadThenNavigateEnvironment { + var mainQueue: AnySchedulerOf +} + +let loadThenNavigateReducer = + counterReducer + .optional() + .pullback( + state: \.optionalCounter, + action: /LoadThenNavigateAction.optionalCounter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + LoadThenNavigateState, LoadThenNavigateAction, LoadThenNavigateEnvironment + > { state, action, environment in + + enum CancelID {} + + switch action { + case .onDisappear: + return .cancel(id: CancelID.self) + + case .setNavigation(isActive: true): + state.isActivityIndicatorVisible = true + return .task { + try await environment.mainQueue.sleep(for: 1) + return .setNavigationIsActiveDelayCompleted + } + .cancellable(id: CancelID.self) + + case .setNavigation(isActive: false): + state.optionalCounter = nil + return .none + + case .setNavigationIsActiveDelayCompleted: + state.isActivityIndicatorVisible = false + state.optionalCounter = CounterState() + return .none + + case .optionalCounter: + return .none + } + } + ) + +struct LoadThenNavigateView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + NavigationLink( + destination: IfLetStore( + self.store.scope( + state: \.optionalCounter, + action: LoadThenNavigateAction.optionalCounter + ) + ) { + CounterView(store: $0) + }, + isActive: viewStore.binding( + get: \.isNavigationActive, + send: LoadThenNavigateAction.setNavigation(isActive:) + ) + ) { + HStack { + Text("Load optional counter") + if viewStore.isActivityIndicatorVisible { + Spacer() + ProgressView() + } + } + } + } + .onDisappear { viewStore.send(.onDisappear) } + } + .navigationTitle("Load then navigate") + } +} + +struct LoadThenNavigateView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + LoadThenNavigateView( + store: Store( + initialState: LoadThenNavigateState(), + reducer: loadThenNavigateReducer, + environment: LoadThenNavigateEnvironment( + mainQueue: .main + ) + ) + ) + } + .navigationViewStyle(.stack) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-NavigateAndLoad.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-NavigateAndLoad.swift new file mode 100755 index 0000000..d4b8242 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-NavigateAndLoad.swift @@ -0,0 +1,115 @@ +import Combine +import ComposableArchitecture +import SwiftUI +import UIKit + +private let readMe = """ + This screen demonstrates navigation that depends on loading optional state. + + Tapping "Load optional counter" simultaneously navigates to a screen that depends on optional \ + counter state and fires off an effect that will load this state a second later. + """ + +struct NavigateAndLoadState: Equatable { + var isNavigationActive = false + var optionalCounter: CounterState? +} + +enum NavigateAndLoadAction: Equatable { + case optionalCounter(CounterAction) + case setNavigation(isActive: Bool) + case setNavigationIsActiveDelayCompleted +} + +struct NavigateAndLoadEnvironment { + var mainQueue: AnySchedulerOf +} + +let navigateAndLoadReducer = + counterReducer + .optional() + .pullback( + state: \.optionalCounter, + action: /NavigateAndLoadAction.optionalCounter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + NavigateAndLoadState, NavigateAndLoadAction, NavigateAndLoadEnvironment + > { state, action, environment in + + enum CancelID {} + + switch action { + case .setNavigation(isActive: true): + state.isNavigationActive = true + return .task { + try await environment.mainQueue.sleep(for: 1) + return .setNavigationIsActiveDelayCompleted + } + .cancellable(id: CancelID.self) + + case .setNavigation(isActive: false): + state.isNavigationActive = false + state.optionalCounter = nil + return .cancel(id: CancelID.self) + + case .setNavigationIsActiveDelayCompleted: + state.optionalCounter = CounterState() + return .none + + case .optionalCounter: + return .none + } + } + ) + +struct NavigateAndLoadView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + NavigationLink( + destination: IfLetStore( + self.store.scope( + state: \.optionalCounter, + action: NavigateAndLoadAction.optionalCounter + ) + ) { + CounterView(store: $0) + } else: { + ProgressView() + }, + isActive: viewStore.binding( + get: \.isNavigationActive, + send: NavigateAndLoadAction.setNavigation(isActive:) + ) + ) { + Text("Load optional counter") + } + } + } + .navigationTitle("Navigate and load") + } +} + +struct NavigateAndLoadView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + NavigateAndLoadView( + store: Store( + initialState: NavigateAndLoadState(), + reducer: navigateAndLoadReducer, + environment: NavigateAndLoadEnvironment( + mainQueue: .main + ) + ) + ) + } + .navigationViewStyle(.stack) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-LoadThenPresent.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-LoadThenPresent.swift new file mode 100755 index 0000000..d44c629 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-LoadThenPresent.swift @@ -0,0 +1,126 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates navigation that depends on loading optional data into state. + + Tapping "Load optional counter" fires off an effect that will load the counter state a second \ + later. When the counter state is present, you will be programmatically presented a sheet that \ + depends on this data. + """ + +struct LoadThenPresentState: Equatable { + var optionalCounter: CounterState? + var isActivityIndicatorVisible = false + + var isSheetPresented: Bool { self.optionalCounter != nil } +} + +enum LoadThenPresentAction { + case onDisappear + case optionalCounter(CounterAction) + case setSheet(isPresented: Bool) + case setSheetIsPresentedDelayCompleted +} + +struct LoadThenPresentEnvironment { + var mainQueue: AnySchedulerOf +} + +let loadThenPresentReducer = + counterReducer + .optional() + .pullback( + state: \.optionalCounter, + action: /LoadThenPresentAction.optionalCounter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + LoadThenPresentState, LoadThenPresentAction, LoadThenPresentEnvironment + > { state, action, environment in + + enum CancelID {} + + switch action { + case .onDisappear: + return .cancel(id: CancelID.self) + + case .setSheet(isPresented: true): + state.isActivityIndicatorVisible = true + return .task { + try await environment.mainQueue.sleep(for: 1) + return .setSheetIsPresentedDelayCompleted + } + .cancellable(id: CancelID.self) + + case .setSheet(isPresented: false): + state.optionalCounter = nil + return .none + + case .setSheetIsPresentedDelayCompleted: + state.isActivityIndicatorVisible = false + state.optionalCounter = CounterState() + return .none + + case .optionalCounter: + return .none + } + } + ) + +struct LoadThenPresentView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + Button(action: { viewStore.send(.setSheet(isPresented: true)) }) { + HStack { + Text("Load optional counter") + if viewStore.isActivityIndicatorVisible { + Spacer() + ProgressView() + } + } + } + } + .sheet( + isPresented: viewStore.binding( + get: \.isSheetPresented, + send: LoadThenPresentAction.setSheet(isPresented:) + ) + ) { + IfLetStore( + self.store.scope( + state: \.optionalCounter, + action: LoadThenPresentAction.optionalCounter + ) + ) { + CounterView(store: $0) + } + } + .navigationTitle("Load and present") + .onDisappear { viewStore.send(.onDisappear) } + } + } +} + +struct LoadThenPresentView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + LoadThenPresentView( + store: Store( + initialState: LoadThenPresentState(), + reducer: loadThenPresentReducer, + environment: LoadThenPresentEnvironment( + mainQueue: .main + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-PresentAndLoad.swift b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-PresentAndLoad.swift new file mode 100755 index 0000000..1deb2c6 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/03-Navigation-Sheet-PresentAndLoad.swift @@ -0,0 +1,114 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates navigation that depends on loading optional data into state. + + Tapping "Load optional counter" simultaneously presents a sheet that depends on optional counter \ + state and fires off an effect that will load this state a second later. + """ + +struct PresentAndLoadState: Equatable { + var optionalCounter: CounterState? + var isSheetPresented = false +} + +enum PresentAndLoadAction { + case optionalCounter(CounterAction) + case setSheet(isPresented: Bool) + case setSheetIsPresentedDelayCompleted +} + +struct PresentAndLoadEnvironment { + var mainQueue: AnySchedulerOf +} + +let presentAndLoadReducer = + counterReducer + .optional() + .pullback( + state: \.optionalCounter, + action: /PresentAndLoadAction.optionalCounter, + environment: { _ in CounterEnvironment() } + ) + .combined( + with: Reducer< + PresentAndLoadState, PresentAndLoadAction, PresentAndLoadEnvironment + > { state, action, environment in + + enum CancelID {} + + switch action { + case .setSheet(isPresented: true): + state.isSheetPresented = true + return .task { + try await environment.mainQueue.sleep(for: 1) + return .setSheetIsPresentedDelayCompleted + } + .cancellable(id: CancelID.self) + + case .setSheet(isPresented: false): + state.isSheetPresented = false + state.optionalCounter = nil + return .cancel(id: CancelID.self) + + case .setSheetIsPresentedDelayCompleted: + state.optionalCounter = CounterState() + return .none + + case .optionalCounter: + return .none + } + } + ) + +struct PresentAndLoadView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + Button("Load optional counter") { + viewStore.send(.setSheet(isPresented: true)) + } + } + .sheet( + isPresented: viewStore.binding( + get: \.isSheetPresented, + send: PresentAndLoadAction.setSheet(isPresented:) + ) + ) { + IfLetStore( + self.store.scope( + state: \.optionalCounter, + action: PresentAndLoadAction.optionalCounter + ) + ) { + CounterView(store: $0) + } else: { + ProgressView() + } + } + .navigationTitle("Present and load") + } + } +} + +struct PresentAndLoadView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + PresentAndLoadView( + store: Store( + initialState: PresentAndLoadState(), + reducer: presentAndLoadReducer, + environment: PresentAndLoadEnvironment( + mainQueue: .main + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift new file mode 100755 index 0000000..0ea4a83 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ElmLikeSubscriptions.swift @@ -0,0 +1,150 @@ +import ComposableArchitecture +@preconcurrency import SwiftUI // NB: SwiftUI.Animation is not Sendable yet. + +private let readMe = """ + This screen demonstrates how the `Reducer` struct can be extended to enhance reducers with \ + extra functionality. + + In this example we introduce a declarative interface for describing long-running effects, \ + inspired by Elm's `subscriptions` API. + """ + +extension Reducer { + static func subscriptions( + _ subscriptions: @escaping (State, Environment) -> [AnyHashable: Effect] + ) -> Self { + var activeSubscriptions: [AnyHashable: Effect] = [:] + + return Reducer { state, _, environment in + let currentSubscriptions = subscriptions(state, environment) + defer { activeSubscriptions = currentSubscriptions } + return .merge( + Set(activeSubscriptions.keys).union(currentSubscriptions.keys).map { id in + switch (activeSubscriptions[id], currentSubscriptions[id]) { + case (.some, .none): + return .cancel(id: id) + case let (.none, .some(effect)): + return effect.cancellable(id: id) + default: + return .none + } + } + ) + } + } +} + +struct ClockState: Equatable { + var isTimerActive = false + var secondsElapsed = 0 +} + +enum ClockAction: Equatable { + case timerTicked + case toggleTimerButtonTapped +} + +struct ClockEnvironment { + var mainQueue: AnySchedulerOf +} + +let clockReducer = Reducer.combine( + Reducer { state, action, environment in + switch action { + case .timerTicked: + state.secondsElapsed += 1 + return .none + case .toggleTimerButtonTapped: + state.isTimerActive.toggle() + return .none + } + }, + .subscriptions { state, environment in + guard state.isTimerActive else { return [:] } + struct TimerID: Hashable {} + return [ + TimerID(): .run { send in + for await _ in environment.mainQueue.timer(interval: 1) { + await send(.timerTicked, animation: .interpolatingSpring(stiffness: 3000, damping: 40)) + } + } + ] + } +) + +struct ClockView: View { + let store: Store + + var body: some View { + WithViewStore(store) { viewStore in + Form { + AboutView(readMe: readMe) + + ZStack { + Circle() + .fill( + AngularGradient( + gradient: Gradient( + colors: [ + .blue.opacity(0.3), + .blue, + .blue, + .green, + .green, + .yellow, + .yellow, + .red, + .red, + .purple, + .purple, + .purple.opacity(0.3), + ] + ), + center: .center + ) + ) + .rotationEffect(.degrees(-90)) + GeometryReader { proxy in + Path { path in + path.move(to: CGPoint(x: proxy.size.width / 2, y: proxy.size.height / 2)) + path.addLine(to: CGPoint(x: proxy.size.width / 2, y: 0)) + } + .stroke(.primary, lineWidth: 3) + .rotationEffect(.degrees(Double(viewStore.secondsElapsed) * 360 / 60)) + } + } + .aspectRatio(1, contentMode: .fit) + .frame(maxWidth: 280) + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + + Button { + viewStore.send(.toggleTimerButtonTapped) + } label: { + Text(viewStore.isTimerActive ? "Stop" : "Start") + .padding(8) + } + .frame(maxWidth: .infinity) + .tint(viewStore.isTimerActive ? Color.red : .accentColor) + .buttonStyle(.borderedProminent) + } + .navigationTitle("Elm-like subscriptions") + } + } +} + +struct Subscriptions_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + ClockView( + store: Store( + initialState: ClockState(), + reducer: clockReducer, + environment: ClockEnvironment( + mainQueue: .main + ) + ) + ) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift new file mode 100755 index 0000000..dfc0886 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Lifecycle.swift @@ -0,0 +1,174 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This demonstrates how to trigger effects when a view appears, and cancel effects when a view \ + disappears. This can be helpful for starting up a feature's long living effects, such as timers, \ + location managers, etc. when that feature is first presented. + + To accomplish this we define a higher-order reducer that enhances any reducer with two additional \ + actions, `.onAppear` and `.onDisappear`, and a way to automate running effects when those actions \ + are sent to the store. + """ + +extension Reducer { + public func lifecycle( + onAppear: @escaping (Environment) -> Effect, + onDisappear: @escaping (Environment) -> Effect + ) -> Reducer, Environment> { + + return .init { state, lifecycleAction, environment in + switch lifecycleAction { + case .onAppear: + return onAppear(environment).map(LifecycleAction.action) + + case .onDisappear: + return onDisappear(environment).fireAndForget() + + case let .action(action): + guard state != nil else { return .none } + return self.run(&state!, action, environment) + .map(LifecycleAction.action) + } + } + } +} + +public enum LifecycleAction { + case onAppear + case onDisappear + case action(Action) +} + +extension LifecycleAction: Equatable where Action: Equatable {} + +struct LifecycleDemoState: Equatable { + var count: Int? +} + +enum LifecycleDemoAction: Equatable { + case timer(LifecycleAction) + case toggleTimerButtonTapped +} + +struct LifecycleDemoEnvironment { + var mainQueue: AnySchedulerOf +} + +let lifecycleDemoReducer: + Reducer = .combine( + timerReducer.pullback( + state: \.count, + action: /LifecycleDemoAction.timer, + environment: { TimerEnvironment(mainQueue: $0.mainQueue) } + ), + Reducer { state, action, environment in + switch action { + case .timer: + return .none + + case .toggleTimerButtonTapped: + state.count = state.count == nil ? 0 : nil + return .none + } + } + ) + +struct LifecycleDemoView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + Button("Toggle Timer") { viewStore.send(.toggleTimerButtonTapped) } + + IfLetStore(self.store.scope(state: \.count, action: LifecycleDemoAction.timer)) { + TimerView(store: $0) + } + } + } + .navigationTitle("Lifecycle") + } +} + +private enum TimerID {} + +enum TimerAction { + case decrementButtonTapped + case incrementButtonTapped + case tick +} + +struct TimerEnvironment { + var mainQueue: AnySchedulerOf +} + +private let timerReducer = Reducer { + state, action, TimerEnvironment in + switch action { + case .decrementButtonTapped: + state -= 1 + return .none + + case .incrementButtonTapped: + state += 1 + return .none + + case .tick: + state += 1 + return .none + } +} +.lifecycle( + onAppear: { environment in + .run { send in + for await _ in environment.mainQueue.timer(interval: 1) { + await send(.tick) + } + } + .cancellable(id: TimerID.self) + }, + onDisappear: { _ in + .cancel(id: TimerID.self) + } +) + +private struct TimerView: View { + let store: Store> + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Section { + Text("Count: \(viewStore.state)") + .onAppear { viewStore.send(.onAppear) } + .onDisappear { viewStore.send(.onDisappear) } + + Button("Decrement") { viewStore.send(.action(.decrementButtonTapped)) } + + Button("Increment") { viewStore.send(.action(.incrementButtonTapped)) } + } + } + } +} + +struct Lifecycle_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationView { + LifecycleDemoView( + store: Store( + initialState: LifecycleDemoState(), + reducer: lifecycleDemoReducer, + environment: LifecycleDemoEnvironment( + mainQueue: .main + ) + ) + ) + } + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Recursion.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Recursion.swift new file mode 100755 index 0000000..e2b0871 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-Recursion.swift @@ -0,0 +1,170 @@ +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how the `Reducer` struct can be extended to enhance reducers with extra \ + functionality. + + In it we introduce an interface for constructing reducers that need to be called recursively in \ + order to handle nested state and actions. It is handed itself as its first argument. + + Tap "Add row" to add a row to the current screen's list. Tap the left-hand side of a row to edit \ + its description, or tap the right-hand side of a row to navigate to its own associated list of \ + rows. + """ + +extension Reducer { + static func recurse( + _ reducer: @escaping (Self, inout State, Action, Environment) -> Effect + ) -> Self { + + var `self`: Self! + self = Self { state, action, environment in + reducer(self, &state, action, environment) + } + return self + } +} + +struct NestedState: Equatable, Identifiable { + var children: IdentifiedArrayOf = [] + let id: UUID + var description: String = "" +} + +indirect enum NestedAction: Equatable { + case append + case node(id: NestedState.ID, action: NestedAction) + case remove(IndexSet) + case rename(String) +} + +struct NestedEnvironment { + var uuid: () -> UUID +} + +let nestedReducer = Reducer< + NestedState, NestedAction, NestedEnvironment +>.recurse { `self`, state, action, environment in + switch action { + case .append: + state.children.append(NestedState(id: environment.uuid())) + return .none + + case .node: + return self.forEach( + state: \.children, + action: /NestedAction.node(id:action:), + environment: { $0 } + ) + .run(&state, action, environment) + + case let .remove(indexSet): + state.children.remove(atOffsets: indexSet) + return .none + + case let .rename(name): + state.description = name + return .none + } +} + +struct NestedView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: \.description) { viewStore in + Form { + Section { + AboutView(readMe: readMe) + } + + ForEachStore( + self.store.scope(state: \.children, action: NestedAction.node(id:action:)) + ) { childStore in + WithViewStore(childStore, observe: \.description) { childViewStore in + NavigationLink( + destination: NestedView(store: childStore) + ) { + HStack { + TextField( + "Untitled", + text: childViewStore.binding(send: NestedAction.rename) + ) + Text("Next") + .font(.callout) + .foregroundStyle(.secondary) + } + } + } + } + .onDelete { viewStore.send(.remove($0)) } + } + .navigationTitle(viewStore.state.isEmpty ? "Untitled" : viewStore.state) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Add row") { viewStore.send(.append) } + } + } + } + } +} + +extension NestedState { + static let mock = NestedState( + children: [ + NestedState( + children: [ + NestedState( + children: [], + id: UUID(), + description: "" + ) + ], + id: UUID(), + description: "Bar" + ), + NestedState( + children: [ + NestedState( + children: [], + id: UUID(), + description: "Fizz" + ), + NestedState( + children: [], + id: UUID(), + description: "Buzz" + ), + ], + id: UUID(), + description: "Baz" + ), + NestedState( + children: [], + id: UUID(), + description: "" + ), + ], + id: UUID(), + description: "Foo" + ) +} + +#if DEBUG + struct NestedView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + NestedView( + store: Store( + initialState: .mock, + reducer: nestedReducer, + environment: NestedEnvironment( + uuid: UUID.init + ) + ) + ) + } + } + } +#endif diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift new file mode 100755 index 0000000..a01a553 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadClient.swift @@ -0,0 +1,46 @@ +import Combine +import ComposableArchitecture +import Foundation +import XCTestDynamicOverlay + +struct DownloadClient { + var download: @Sendable (URL) -> AsyncThrowingStream + + enum Event: Equatable { + case response(Data) + case updateProgress(Double) + } +} + +extension DownloadClient { + static let live = DownloadClient( + download: { url in + .init { continuation in + Task { + do { + let (bytes, response) = try await URLSession.shared.bytes(from: url) + var data = Data() + var progress = 0 + for try await byte in bytes { + data.append(byte) + let newProgress = Int( + Double(data.count) / Double(response.expectedContentLength) * 100) + if newProgress != progress { + progress = newProgress + continuation.yield(.updateProgress(Double(progress) / 100)) + } + } + continuation.yield(.response(data)) + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + } + } + ) + + static let unimplemented = Self( + download: XCTUnimplemented("\(Self.self).asyncDownload") + ) +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift new file mode 100755 index 0000000..b5169d0 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/DownloadComponent.swift @@ -0,0 +1,184 @@ +import ComposableArchitecture +@preconcurrency import SwiftUI // NB: SwiftUI.Animation is not Sendable yet. + +struct DownloadComponentState: Equatable { + var alert: AlertState? + let id: ID + var mode: Mode + let url: URL +} + +enum Mode: Equatable { + case downloaded + case downloading(progress: Double) + case notDownloaded + case startingToDownload + + var progress: Double { + if case let .downloading(progress) = self { return progress } + return 0 + } + + var isDownloading: Bool { + switch self { + case .downloaded, .notDownloaded: + return false + case .downloading, .startingToDownload: + return true + } + } +} + +enum DownloadComponentAction: Equatable { + case alert(AlertAction) + case buttonTapped + case downloadClient(TaskResult) + + enum AlertAction: Equatable { + case deleteButtonTapped + case dismissed + case nevermindButtonTapped + case stopButtonTapped + } +} + +struct DownloadComponentEnvironment { + var downloadClient: DownloadClient +} + +extension Reducer { + func downloadable( + state: WritableKeyPath>, + action: CasePath, + environment: @escaping (Environment) -> DownloadComponentEnvironment + ) -> Self { + .combine( + Reducer, DownloadComponentAction, DownloadComponentEnvironment> { + state, action, environment in + switch action { + case .alert(.deleteButtonTapped): + state.alert = nil + state.mode = .notDownloaded + return .none + + case .alert(.nevermindButtonTapped), + .alert(.dismissed): + state.alert = nil + return .none + + case .alert(.stopButtonTapped): + state.mode = .notDownloaded + state.alert = nil + return .cancel(id: state.id) + + case .buttonTapped: + switch state.mode { + case .downloaded: + state.alert = deleteAlert + return .none + + case .downloading: + state.alert = stopAlert + return .none + + case .notDownloaded: + state.mode = .startingToDownload + + return .run { [url = state.url] send in + for try await event in environment.downloadClient.download(url) { + await send(.downloadClient(.success(event)), animation: .default) + } + } catch: { error, send in + await send(.downloadClient(.failure(error)), animation: .default) + } + .cancellable(id: state.id) + + case .startingToDownload: + state.alert = stopAlert + return .none + } + + case .downloadClient(.success(.response)): + state.mode = .downloaded + state.alert = nil + return .none + + case let .downloadClient(.success(.updateProgress(progress))): + state.mode = .downloading(progress: progress) + return .none + + case .downloadClient(.failure): + state.mode = .notDownloaded + state.alert = nil + return .none + } + } + .pullback(state: state, action: action, environment: environment), + self + ) + } +} + +private let deleteAlert = AlertState( + title: TextState("Do you want to delete this map from your offline storage?"), + primaryButton: .destructive( + TextState("Delete"), + action: .send(.deleteButtonTapped, animation: .default) + ), + secondaryButton: nevermindButton +) + +private let stopAlert = AlertState( + title: TextState("Do you want to stop downloading this map?"), + primaryButton: .destructive( + TextState("Stop"), + action: .send(.stopButtonTapped, animation: .default) + ), + secondaryButton: nevermindButton +) + +let nevermindButton = AlertState.Button + .cancel(TextState("Nevermind"), action: .send(.nevermindButtonTapped)) + +struct DownloadComponent: View { + let store: Store, DownloadComponentAction> + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Button { + viewStore.send(.buttonTapped) + } label: { + if viewStore.mode == .downloaded { + Image(systemName: "checkmark.circle") + .tint(.accentColor) + } else if viewStore.mode.progress > 0 { + ZStack { + CircularProgressView(value: viewStore.mode.progress) + .frame(width: 16, height: 16) + Rectangle() + .frame(width: 6, height: 6) + } + } else if viewStore.mode == .notDownloaded { + Image(systemName: "icloud.and.arrow.down") + } else if viewStore.mode == .startingToDownload { + ZStack { + ProgressView() + Rectangle() + .frame(width: 6, height: 6) + } + } + } + .foregroundStyle(.primary) + .alert( + self.store.scope(state: \.alert, action: DownloadComponentAction.alert), + dismiss: .dismissed + ) + } + } +} + +struct DownloadComponent_Previews: PreviewProvider { + static var previews: some View { + DownloadList_Previews.previews + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift new file mode 100755 index 0000000..6a52842 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ResuableOfflineDownloads/ReusableComponents-Download.swift @@ -0,0 +1,293 @@ +import Combine +import ComposableArchitecture +import SwiftUI + +private let readMe = """ + This screen demonstrates how one can create reusable components in the Composable Architecture. + + The "download component" is a component that can be added to any view to enhance it with the \ + concept of downloading offline content. It facilitates downloading the data, displaying a \ + progress view while downloading, canceling an active download, and deleting previously \ + downloaded data. + + Tap the download icon to start a download, and tap again to cancel an in-flight download or to \ + remove a finished download. While a file is downloading you can tap a row to go to another \ + screen to see that the state is carried over. + """ + +struct CityMap: Equatable, Identifiable { + var blurb: String + var downloadVideoUrl: URL + let id: UUID + var title: String +} + +struct CityMapState: Equatable, Identifiable { + var downloadAlert: AlertState? + var downloadMode: Mode + var cityMap: CityMap + + var id: UUID { self.cityMap.id } + + var downloadComponent: DownloadComponentState { + get { + DownloadComponentState( + alert: self.downloadAlert, + id: self.cityMap.id, + mode: self.downloadMode, + url: self.cityMap.downloadVideoUrl + ) + } + set { + self.downloadAlert = newValue.alert + self.downloadMode = newValue.mode + } + } +} + +enum CityMapAction { + case downloadComponent(DownloadComponentAction) +} + +struct CityMapEnvironment { + var downloadClient: DownloadClient + var mainQueue: AnySchedulerOf +} + +let cityMapReducer = Reducer { + state, action, environment in + switch action { + case let .downloadComponent(.downloadClient(.success(.response(data)))): + // NB: This is where you could perform the effect to save the data to a file on disk. + return .none + + case .downloadComponent(.alert(.deleteButtonTapped)): + // NB: This is where you could perform the effect to delete the data from disk. + return .none + + case .downloadComponent: + return .none + } +} +.downloadable( + state: \.downloadComponent, + action: /CityMapAction.downloadComponent, + environment: { DownloadComponentEnvironment(downloadClient: $0.downloadClient) } +) + +struct CityMapRowView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + HStack { + NavigationLink( + destination: CityMapDetailView(store: self.store) + ) { + HStack { + Image(systemName: "map") + Text(viewStore.cityMap.title) + } + .layoutPriority(1) + + Spacer() + + DownloadComponent( + store: self.store.scope( + state: \.downloadComponent, + action: CityMapAction.downloadComponent + ) + ) + .padding(.trailing, 8) + } + } + } + } +} + +struct CityMapDetailView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + VStack(spacing: 32) { + Text(viewStore.cityMap.blurb) + + HStack { + if viewStore.downloadMode == .notDownloaded { + Text("Download for offline viewing") + } else if viewStore.downloadMode == .downloaded { + Text("Downloaded") + } else { + Text("Downloading \(Int(100 * viewStore.downloadComponent.mode.progress))%") + } + + Spacer() + + DownloadComponent( + store: self.store.scope( + state: \.downloadComponent, + action: CityMapAction.downloadComponent + ) + ) + } + + Spacer() + } + .navigationTitle(viewStore.cityMap.title) + .padding() + } + } +} + +struct MapAppState: Equatable { + var cityMaps: IdentifiedArrayOf +} + +enum MapAppAction { + case cityMaps(id: CityMapState.ID, action: CityMapAction) +} + +struct MapAppEnvironment { + var downloadClient: DownloadClient + var mainQueue: AnySchedulerOf +} + +let mapAppReducer: Reducer = cityMapReducer.forEach( + state: \MapAppState.cityMaps, + action: /MapAppAction.cityMaps(id:action:), + environment: { + CityMapEnvironment( + downloadClient: $0.downloadClient, + mainQueue: $0.mainQueue + ) + } +) + +struct CitiesView: View { + let store: Store + + var body: some View { + Form { + Section { + AboutView(readMe: readMe) + } + ForEachStore( + self.store.scope(state: \.cityMaps, action: MapAppAction.cityMaps(id:action:)) + ) { cityMapStore in + CityMapRowView(store: cityMapStore) + .buttonStyle(.borderless) + } + } + .navigationTitle("Offline Downloads") + } +} + +struct DownloadList_Previews: PreviewProvider { + static var previews: some View { + Group { + NavigationView { + CitiesView( + store: Store( + initialState: MapAppState(cityMaps: .mocks), + reducer: mapAppReducer, + environment: MapAppEnvironment( + downloadClient: .live, + mainQueue: .main + ) + ) + ) + } + + NavigationView { + CityMapDetailView( + store: Store( + initialState: IdentifiedArray.mocks.first!, + reducer: .empty, + environment: () + ) + ) + } + } + } +} + +extension IdentifiedArray where ID == CityMapState.ID, Element == CityMapState { + static let mocks: Self = [ + CityMapState( + downloadMode: .notDownloaded, + cityMap: CityMap( + blurb: """ + New York City (NYC), known colloquially as New York (NY) and officially as the City of \ + New York, is the most populous city in the United States. With an estimated 2018 \ + population of 8,398,748 distributed over about 302.6 square miles (784 km2), New York \ + is also the most densely populated major city in the United States. + """, + downloadVideoUrl: URL(string: "http://ipv4.download.thinkbroadband.com/50MB.zip")!, + id: UUID(), + title: "New York, NY" + ) + ), + CityMapState( + downloadMode: .notDownloaded, + cityMap: CityMap( + blurb: """ + Los Angeles, officially the City of Los Angeles and often known by its initials L.A., \ + is the largest city in the U.S. state of California. With an estimated population of \ + nearly four million people, it is the country's second most populous city (after New \ + York City) and the third most populous city in North America (after Mexico City and \ + New York City). Los Angeles is known for its Mediterranean climate, ethnic diversity, \ + Hollywood entertainment industry, and its sprawling metropolis. + """, + downloadVideoUrl: URL(string: "http://ipv4.download.thinkbroadband.com/50MB.zip")!, + id: UUID(), + title: "Los Angeles, LA" + ) + ), + CityMapState( + downloadMode: .notDownloaded, + cityMap: CityMap( + blurb: """ + Paris is the capital and most populous city of France, with a population of 2,148,271 \ + residents (official estimate, 1 January 2020) in an area of 105 square kilometres (41 \ + square miles). Since the 17th century, Paris has been one of Europe's major centres of \ + finance, diplomacy, commerce, fashion, science and arts. + """, + downloadVideoUrl: URL(string: "http://ipv4.download.thinkbroadband.com/50MB.zip")!, + id: UUID(), + title: "Paris, France" + ) + ), + CityMapState( + downloadMode: .notDownloaded, + cityMap: CityMap( + blurb: """ + Tokyo, officially Tokyo Metropolis (東京都, Tōkyō-to), is the capital of Japan and the \ + most populous of the country's 47 prefectures. Located at the head of Tokyo Bay, the \ + prefecture forms part of the Kantō region on the central Pacific coast of Japan's main \ + island, Honshu. Tokyo is the political, economic, and cultural center of Japan, and \ + houses the seat of the Emperor and the national government. + """, + downloadVideoUrl: URL(string: "http://ipv4.download.thinkbroadband.com/50MB.zip")!, + id: UUID(), + title: "Tokyo, Japan" + ) + ), + CityMapState( + downloadMode: .notDownloaded, + cityMap: CityMap( + blurb: """ + Buenos Aires is the capital and largest city of Argentina. The city is located on the \ + western shore of the estuary of the Río de la Plata, on the South American continent's \ + southeastern coast. "Buenos Aires" can be translated as "fair winds" or "good airs", \ + but the former was the meaning intended by the founders in the 16th century, by the \ + use of the original name "Real de Nuestra Señora Santa María del Buen Ayre", named \ + after the Madonna of Bonaria in Sardinia. + """, + downloadVideoUrl: URL(string: "http://ipv4.download.thinkbroadband.com/50MB.zip")!, + id: UUID(), + title: "Buenos Aires, Argentina" + ) + ), + ] +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift new file mode 100755 index 0000000..f9a8955 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/04-HigherOrderReducers-ReusableFavoriting.swift @@ -0,0 +1,225 @@ +import Combine +import ComposableArchitecture +import Foundation +import SwiftUI + +private let readMe = """ + This screen demonstrates how one can create reusable components in the Composable Architecture. + + It introduces the domain, logic, and view around "favoriting" something, which is considerably \ + complex. + + A feature can give itself the ability to "favorite" part of its state by embedding the domain of \ + favoriting, using the `favorite` higher-order reducer, and passing an appropriately scoped store \ + to `FavoriteButton`. + + Tapping the favorite button on a row will instantly reflect in the UI and fire off an effect to \ + do any necessary work, like writing to a database or making an API request. We have simulated a \ + request that takes 1 second to run and may fail 25% of the time. Failures result in rolling back \ + favorite state and rendering an alert. + """ + +// MARK: - Favorite domain + +struct FavoriteState: Equatable, Identifiable { + var alert: AlertState? + let id: ID + var isFavorite: Bool +} + +enum FavoriteAction: Equatable { + case alertDismissed + case buttonTapped + case response(TaskResult) +} + +struct FavoriteEnvironment { + var request: @Sendable (ID, Bool) async throws -> Bool +} + +/// A cancellation token that cancels in-flight favoriting requests. +struct FavoriteCancelID: Hashable { + var id: ID +} + +extension Reducer { + /// Enhances a reducer with favoriting logic. + func favorite( + state: WritableKeyPath>, + action: CasePath, + environment: @escaping (Environment) -> FavoriteEnvironment + ) -> Self { + .combine( + self, + Reducer, FavoriteAction, FavoriteEnvironment> { + state, action, environment in + switch action { + case .alertDismissed: + state.alert = nil + state.isFavorite.toggle() + return .none + + case .buttonTapped: + state.isFavorite.toggle() + + return .task { [id = state.id, isFavorite = state.isFavorite] in + await .response(TaskResult { try await environment.request(id, isFavorite) }) + } + .cancellable(id: FavoriteCancelID(id: state.id), cancelInFlight: true) + + case let .response(.failure(error)): + state.alert = AlertState(title: TextState(error.localizedDescription)) + return .none + + case let .response(.success(isFavorite)): + state.isFavorite = isFavorite + return .none + } + } + .pullback(state: state, action: action, environment: environment) + ) + } +} + +struct FavoriteButton: View { + let store: Store, FavoriteAction> + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Button { + viewStore.send(.buttonTapped) + } label: { + Image(systemName: "heart") + .symbolVariant(viewStore.isFavorite ? .fill : .none) + } + .alert(self.store.scope(state: \.alert), dismiss: .alertDismissed) + } + } +} + +// MARK: Feature domain - + +struct EpisodeState: Equatable, Identifiable { + var alert: AlertState? + let id: UUID + var isFavorite: Bool + let title: String + + var favorite: FavoriteState { + get { .init(alert: self.alert, id: self.id, isFavorite: self.isFavorite) } + set { (self.alert, self.isFavorite) = (newValue.alert, newValue.isFavorite) } + } +} + +enum EpisodeAction: Equatable { + case favorite(FavoriteAction) +} + +struct EpisodeEnvironment { + var favorite: @Sendable (EpisodeState.ID, Bool) async throws -> Bool +} + +struct EpisodeView: View { + let store: Store + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + HStack(alignment: .firstTextBaseline) { + Text(viewStore.title) + + Spacer() + + FavoriteButton( + store: self.store.scope(state: \.favorite, action: EpisodeAction.favorite)) + } + } + } +} + +let episodeReducer = Reducer.empty.favorite( + state: \.favorite, + action: /EpisodeAction.favorite, + environment: { FavoriteEnvironment(request: $0.favorite) } +) + +struct EpisodesState: Equatable { + var episodes: IdentifiedArrayOf = [] +} + +enum EpisodesAction: Equatable { + case episode(id: EpisodeState.ID, action: EpisodeAction) +} + +struct EpisodesEnvironment { + var favorite: @Sendable (UUID, Bool) async throws -> Bool +} + +let episodesReducer: Reducer = + episodeReducer.forEach( + state: \EpisodesState.episodes, + action: /EpisodesAction.episode(id:action:), + environment: { EpisodeEnvironment(favorite: $0.favorite) } + ) + +struct EpisodesView: View { + let store: Store + + var body: some View { + Form { + Section { + AboutView(readMe: readMe) + } + ForEachStore( + self.store.scope(state: \.episodes, action: EpisodesAction.episode(id:action:)) + ) { rowStore in + EpisodeView(store: rowStore) + } + .buttonStyle(.borderless) + } + .navigationTitle("Favoriting") + } +} + +struct EpisodesView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + EpisodesView( + store: Store( + initialState: EpisodesState( + episodes: .mocks + ), + reducer: episodesReducer, + environment: EpisodesEnvironment( + favorite: favorite(id:isFavorite:) + ) + ) + ) + } + } +} + +struct FavoriteError: LocalizedError { + var errorDescription: String? { + "Favoriting failed." + } +} + +@Sendable func favorite(id: ID, isFavorite: Bool) async throws -> Bool { + try await Task.sleep(nanoseconds: NSEC_PER_SEC) + if .random(in: 0...1) > 0.25 { + return isFavorite + } else { + throw FavoriteError() + } +} + +extension IdentifiedArray where ID == EpisodeState.ID, Element == EpisodeState { + static let mocks: Self = [ + EpisodeState(id: UUID(), isFavorite: false, title: "Functions"), + EpisodeState(id: UUID(), isFavorite: false, title: "Side Effects"), + EpisodeState(id: UUID(), isFavorite: false, title: "Algebraic Data Types"), + EpisodeState(id: UUID(), isFavorite: false, title: "DSLs"), + EpisodeState(id: UUID(), isFavorite: false, title: "Parsers"), + EpisodeState(id: UUID(), isFavorite: false, title: "Composable Architecture"), + ] +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..b1516e5a122b534ae5a9d14706d6248718aeb03a GIT binary patch literal 7569 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 literal 0 HcmV?d00001 diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-76@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..869cd81f3aee14834f60b9fb9bb87b3ec53a2840 GIT binary patch literal 9893 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 XN$)>5c`(j700000NkvXXu0mjfLN#h7 literal 0 HcmV?d00001 diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon-iPadPro@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..94a32fd65c0f0b6cd8af035b2d10286b49c8732b GIT binary patch literal 15226 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 literal 0 HcmV?d00001 diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100755 index 0000000..4f45077 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,103 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon-60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "AppIcon-iPadPro@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "transparent.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/transparent.png new file mode 100755 index 0000000000000000000000000000000000000000..bae1e0d7424038acc03d0981da831baf5752e49e GIT binary patch literal 221 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7#M*hLwK5)9*|-x3GxeOU?`h>)&j_z=IP=X tQo;E4U?WJ$;RU~g String +} + +// This is the "live" fact dependency that reaches into the outside world to fetch trivia. +// Typically this live implementation of the dependency would live in its own module so that the +// main feature doesn't need to compile it. +extension FactClient { + static let live = Self( + fetch: { number in + try await Task.sleep(nanoseconds: NSEC_PER_SEC) + let (data, _) = try await URLSession.shared + .data(from: URL(string: "http://numbersapi.com/\(number)/trivia")!) + return String(decoding: data, as: UTF8.self) + } + ) +} + +#if DEBUG + extension FactClient { + // This is the "unimplemented" fact dependency that is useful to plug into tests that you want + // to prove do not need the dependency. + static let unimplemented = Self( + fetch: XCTUnimplemented("\(Self.self).fetch") + ) + } +#endif diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Info.plist b/Examples/CaseStudies/SwiftUICaseStudies/Info.plist new file mode 100755 index 0000000..b94c796 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Info.plist @@ -0,0 +1,48 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + UILaunchStoryboardName + LaunchScreen + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Internal/AboutView.swift b/Examples/CaseStudies/SwiftUICaseStudies/Internal/AboutView.swift new file mode 100755 index 0000000..6ca6245 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Internal/AboutView.swift @@ -0,0 +1,11 @@ +import SwiftUI + +struct AboutView: View { + let readMe: String + + var body: some View { + DisclosureGroup("About this case study") { + Text(template: self.readMe) + } + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Internal/CircularProgressView.swift b/Examples/CaseStudies/SwiftUICaseStudies/Internal/CircularProgressView.swift new file mode 100755 index 0000000..0ca6237 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Internal/CircularProgressView.swift @@ -0,0 +1,23 @@ +import SwiftUI + +struct CircularProgressView: View { + private let value: Double + + init(value: Double) { + self.value = value + } + + var body: some View { + Circle() + .trim(from: 0, to: CGFloat(self.value)) + .stroke(style: StrokeStyle(lineWidth: 2, lineCap: .round)) + .rotationEffect(.degrees(-90)) + .animation(.easeIn, value: self.value) + } +} + +struct CircularProgressView_Previews: PreviewProvider { + static var previews: some View { + CircularProgressView(value: 0.3).frame(width: 44, height: 44) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift b/Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift new file mode 100755 index 0000000..fce58e0 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Internal/ResignFirstResponder.swift @@ -0,0 +1,21 @@ +import SwiftUI + +extension Binding { + /// SwiftUI will print errors to the console about "AttributeGraph: cycle detected" if you disable + /// a text field while it is focused. This hack will force all fields to unfocus before we write + /// to a binding that may disable the fields. + /// + /// See also: https://stackoverflow.com/a/69653555 + @MainActor + func resignFirstResponder() -> Self { + Self( + get: { self.wrappedValue }, + set: { newValue, transaction in + UIApplication.shared.sendAction( + #selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil + ) + self.transaction(transaction).wrappedValue = newValue + } + ) + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Internal/TemplateText.swift b/Examples/CaseStudies/SwiftUICaseStudies/Internal/TemplateText.swift new file mode 100755 index 0000000..e5fb1ca --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Internal/TemplateText.swift @@ -0,0 +1,59 @@ +import SwiftUI + +extension Text { + init(template: String, _ style: Font.TextStyle = .body) { + enum Style: Hashable { + case code + case emphasis + case strong + } + + var segments: [Text] = [] + var currentValue = "" + var currentStyles: Set