From 454142f4ef11f13d78f197a84de228ccbb1262e5 Mon Sep 17 00:00:00 2001 From: Mike Packard Date: Sat, 9 Oct 2021 04:48:01 +0700 Subject: [PATCH] reactiveswift-composable-architecture Signed-off-by: Mike Packard --- .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 11762 bytes .../xcschemes/ComposableArchitecture.xcscheme | 67 + ...t-composable-architecture-Package.xcscheme | 114 ++ ...composable-architecture-benchmark.xcscheme | 102 ++ .../xcschemes/xcschememanagement.plist | 87 ++ .../contents.xcworkspacedata | 13 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/swiftpm/Package.resolved | 124 ++ .../UserInterfaceState.xcuserstate | Bin 0 -> 272511 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 6 + .../xcschemes/xcschememanagement.plist | 261 ++++ .../CaseStudies.xcodeproj/project.pbxproj | 772 +++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/CaseStudies(SwiftUI).xcscheme | 78 ++ .../xcschemes/CaseStudies(UIKit).xcscheme | 78 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8152 bytes .../AppIcon.appiconset/Contents.json | 99 ++ .../Assets.xcassets/Contents.json | 6 + .../SwiftUICaseStudies/ContentView.swift | 14 + .../Preview Assets.xcassets/Contents.json | 6 + .../SwiftUICaseStudiesApp.swift | 10 + .../UIKitCaseStudies/AppDelegate.swift | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/AppIcon.png | Bin 0 -> 8152 bytes .../AppIcon.appiconset/Contents.json | 99 ++ .../Assets.xcassets/Contents.json | 6 + .../AuthScreen/AuthAction.swift | 11 + .../AuthScreen/AuthEnvironment.swift | 6 + .../AuthScreen/AuthReducer.swift | 22 + .../AuthScreen/AuthState.swift | 6 + .../AuthScreen/AuthViewController.swift | 117 ++ .../Base.lproj/LaunchScreen.storyboard | 25 + .../Base.lproj/Main.storyboard | 76 ++ .../CounterScreen/CounterAction.swift | 11 + .../CounterScreen/CounterEnvironment.swift | 6 + .../CounterScreen/CounterReducer.swift | 24 + .../CounterScreen/CounterState.swift | 6 + .../CounterScreen/CounterViewController.swift | 129 ++ .../CountersTableAction.swift | 10 + .../CountersTableEnvironment.swift | 6 + .../CountersTableReducer.swift | 22 + .../CountersTableState.swift | 11 + .../CountersTableViewController.swift | 133 ++ .../EagerNavigationAction.swift | 12 + .../EagerNavigationEnvironment.swift | 6 + .../EagerNavigationReducer.swift | 38 + .../EagerNavigationState.swift | 7 + .../EagerNavigationViewController.swift | 144 ++ .../CaseStudies/UIKitCaseStudies/Info.plist | 25 + .../ActivityIndicatorViewController.swift | 20 + .../Internal/IfLetStoreController.swift | 50 + .../LazyNavigation/LazyNavigationAction.swift | 12 + .../LazyNavigationEnvironment.swift | 6 + .../LazyNavigationReducer.swift | 38 + .../LazyNavigation/LazyNavigationState.swift | 7 + .../LazyNavigationViewController.swift | 146 +++ .../MainScreen/MainAction.swift | 11 + .../MainScreen/MainEnvironment.swift | 6 + .../MainScreen/MainReducer.swift | 22 + .../MainScreen/MainState.swift | 6 + .../MainScreen/MainViewController.swift | 166 +++ .../RootScreen/RootAction.swift | 11 + .../RootScreen/RootEnvironment.swift | 6 + .../RootScreen/RootReducer.swift | 29 + .../RootScreen/RootState.swift | 13 + .../RootScreen/RootViewController.swift | 126 ++ .../UIKitCaseStudies/SceneDelegate.swift | 13 + Examples/Package.swift | 9 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 148 +++ .../Shared/Assets.xcassets/Contents.json | 6 + .../TodoApp/AuthScreen/AuthAction.swift | 10 + .../TodoApp/AuthScreen/AuthEnvironment.swift | 6 + .../TodoApp/AuthScreen/AuthReducer.swift | 20 + .../Shared/TodoApp/AuthScreen/AuthState.swift | 6 + .../Shared/TodoApp/AuthScreen/AuthView.swift | 89 ++ .../TodoApp/CounterScreen/CounterAction.swift | 10 + .../CounterScreen/CounterEnvironment.swift | 6 + .../CounterScreen/CounterReducer.swift | 21 + .../TodoApp/CounterScreen/CounterState.swift | 6 + .../TodoApp/CounterScreen/CounterView.swift | 99 ++ .../TodoApp/MainScreen/MainAction.swift | 21 + .../TodoApp/MainScreen/MainEnvironment.swift | 6 + .../TodoApp/MainScreen/MainReducer.swift | 99 ++ .../Shared/TodoApp/MainScreen/MainState.swift | 9 + .../Shared/TodoApp/MainScreen/MainView.swift | 193 +++ .../Todos/Shared/TodoApp/Models/Todo.swift | 7 + .../TodoApp/RootScreen/RootAction.swift | 10 + .../TodoApp/RootScreen/RootEnvironment.swift | 6 + .../TodoApp/RootScreen/RootReducer.swift | 27 + .../Shared/TodoApp/RootScreen/RootState.swift | 13 + .../Shared/TodoApp/RootScreen/RootView.swift | 90 ++ .../Todos/Shared/TodoApp/View+/View+.swift | 9 + Examples/Todos/Shared/TodosApp.swift | 13 + .../Todos/Todos.xcodeproj/project.pbxproj | 750 +++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcschemes/Todos (iOS).xcscheme | 78 ++ .../xcschemes/Todos (macOS).xcscheme | 78 ++ Examples/Todos/macOS/macOS.entitlements | 14 + Package.resolved | 88 ++ Package.swift | 50 + README.md | 19 + .../Beta/Combine+ReactiveSwift.swift | 28 + .../Beta/Concurrency.swift | 103 ++ .../Debugging/ReducerDebugging.swift | 111 ++ .../Debugging/ReducerInstrumentation.swift | 95 ++ Sources/ComposableArchitecture/Effect.swift | 93 ++ .../Effects/Cancellation.swift | 81 ++ .../Effects/Debouncing.swift | 16 + .../Effects/Deferring.swift | 13 + .../Effects/Throttling.swift | 55 + .../Effects/Timer.swift | 16 + .../Internal/Breakpoint.swift | 32 + .../Internal/Debug.swift | 216 +++ .../Internal/Deprecations.swift | 415 ++++++ .../Internal/Diff.swift | 82 ++ .../Internal/Exports.swift | 5 + .../Internal/Locking.swift | 21 + Sources/ComposableArchitecture/Reducer.swift | 253 ++++ Sources/ComposableArchitecture/Store.swift | 210 +++ .../SwiftUI/ActionSheet.swift | 96 ++ .../SwiftUI/ActionWrappingScheduler.swift | 107 ++ .../SwiftUI/Alert.swift | 225 ++++ .../SwiftUI/Animation.swift | 12 + .../SwiftUI/Binding.swift | 178 +++ .../SwiftUI/ForEachStore.swift | 42 + .../SwiftUI/Identified.swift | 33 + .../SwiftUI/IfLetStore.swift | 57 + .../SwiftUI/SwitchStore.swift | 1163 +++++++++++++++++ .../SwiftUI/TextState.swift | 337 +++++ .../SwiftUI/WithViewStore.swift | 181 +++ .../UIKit/IfLetUIKit.swift | 22 + .../UIKit/UIKitAnimationScheduler.swift | 177 +++ .../UIKit/UIViewRepresented.swift | 21 + .../ComposableArchitecture/ViewStore.swift | 161 +++ .../main.swift | 43 + .../ComposableArchitectureTests.swift | 1 + .../DebugTests.swift | 1 + .../EffectCancellationTests.swift | 1 + .../EffectDebounceTests.swift | 1 + .../EffectDeferredTests.swift | 1 + .../EffectTests.swift | 1 + .../EffectThrottleTests.swift | 1 + Tests/ComposableArchitectureTests/LCRNG.swift | 15 + .../MemoryManagementTests.swift | 1 + .../ReducerTests.swift | 1 + .../StoreTests.swift | 1 + .../TestStoreTests.swift | 1 + .../TimerTests.swift | 1 + .../ViewStoreTests.swift | 1 + .../WithViewStoreAppTest.swift | 1 + Tests/LinuxMain.swift | 2 + 157 files changed, 10323 insertions(+) create mode 100644 .swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata create mode 100644 .swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 .swiftpm/xcode/package.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/reactiveswift-composable-architecture-Package.xcscheme create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme create mode 100644 .swiftpm/xcode/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 ComposableArchitecture.xcworkspace/contents.xcworkspacedata create mode 100644 ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved create mode 100644 ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj create mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(SwiftUI).xcscheme create mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(UIKit).xcscheme create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/AppDelegate.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json create mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Info.plist create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootAction.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootEnvironment.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootState.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootViewController.swift create mode 100644 Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift create mode 100644 Examples/Package.swift create mode 100644 Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/Todos/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/Todos/Shared/Assets.xcassets/Contents.json create mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift create mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthEnvironment.swift create mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift create mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthState.swift create mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthView.swift create mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterAction.swift create mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterEnvironment.swift create mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift create mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterState.swift create mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterView.swift create mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainAction.swift create mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainEnvironment.swift create mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift create mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainState.swift create mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainView.swift create mode 100644 Examples/Todos/Shared/TodoApp/Models/Todo.swift create mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootAction.swift create mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootEnvironment.swift create mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift create mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootState.swift create mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootView.swift create mode 100644 Examples/Todos/Shared/TodoApp/View+/View+.swift create mode 100644 Examples/Todos/Shared/TodosApp.swift create mode 100644 Examples/Todos/Todos.xcodeproj/project.pbxproj create mode 100644 Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme create mode 100644 Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme create mode 100644 Examples/Todos/macOS/macOS.entitlements create mode 100644 Package.resolved create mode 100644 Package.swift create mode 100644 README.md create mode 100644 Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift create mode 100644 Sources/ComposableArchitecture/Beta/Concurrency.swift create mode 100644 Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift create mode 100644 Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift create mode 100644 Sources/ComposableArchitecture/Effect.swift create mode 100644 Sources/ComposableArchitecture/Effects/Cancellation.swift create mode 100644 Sources/ComposableArchitecture/Effects/Debouncing.swift create mode 100644 Sources/ComposableArchitecture/Effects/Deferring.swift create mode 100644 Sources/ComposableArchitecture/Effects/Throttling.swift create mode 100644 Sources/ComposableArchitecture/Effects/Timer.swift create mode 100644 Sources/ComposableArchitecture/Internal/Breakpoint.swift create mode 100644 Sources/ComposableArchitecture/Internal/Debug.swift create mode 100644 Sources/ComposableArchitecture/Internal/Deprecations.swift create mode 100644 Sources/ComposableArchitecture/Internal/Diff.swift create mode 100644 Sources/ComposableArchitecture/Internal/Exports.swift create mode 100644 Sources/ComposableArchitecture/Internal/Locking.swift create mode 100644 Sources/ComposableArchitecture/Reducer.swift create mode 100644 Sources/ComposableArchitecture/Store.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/ActionWrappingScheduler.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/Alert.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/Animation.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/Binding.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/Identified.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/TextState.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift create mode 100644 Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift create mode 100644 Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift create mode 100644 Sources/ComposableArchitecture/UIKit/UIViewRepresented.swift create mode 100644 Sources/ComposableArchitecture/ViewStore.swift create mode 100644 Sources/swift-composable-architecture-benchmark/main.swift create mode 100644 Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift create mode 100644 Tests/ComposableArchitectureTests/DebugTests.swift create mode 100644 Tests/ComposableArchitectureTests/EffectCancellationTests.swift create mode 100644 Tests/ComposableArchitectureTests/EffectDebounceTests.swift create mode 100644 Tests/ComposableArchitectureTests/EffectDeferredTests.swift create mode 100644 Tests/ComposableArchitectureTests/EffectTests.swift create mode 100644 Tests/ComposableArchitectureTests/EffectThrottleTests.swift create mode 100644 Tests/ComposableArchitectureTests/LCRNG.swift create mode 100644 Tests/ComposableArchitectureTests/MemoryManagementTests.swift create mode 100644 Tests/ComposableArchitectureTests/ReducerTests.swift create mode 100644 Tests/ComposableArchitectureTests/StoreTests.swift create mode 100644 Tests/ComposableArchitectureTests/TestStoreTests.swift create mode 100644 Tests/ComposableArchitectureTests/TimerTests.swift create mode 100644 Tests/ComposableArchitectureTests/ViewStoreTests.swift create mode 100644 Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift create mode 100644 Tests/LinuxMain.swift 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/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..556338554164076db2c21ed454183354c223023e GIT binary patch literal 11762 zcmeHNd3aORw%>c7rWp$6q>waC+RO=4=avqjKu4%RTj&6kA*Sh}jigD)08}ozaljcT zP$q@8OrnTfoGzjug5toHLB;VpD~hOBalnbU&N)d3sQ2T&@4f#X{m2>i-fOSD_gcR- zou+`-6AEW#y^b&LP4c&f97{ekH=f76wmI~+$hC9L#nDbjkqpV}Ys0QENAV67W>5kefRd0FC8Kndfih7R%0@XT7v-Um zC?8#lE<>fL5?zjJP%HAFE73gUMQzB3{3w9tqaa#B5pcQB( zT7}l4b?APy32jDO&^Gia+KK*-UPiB=SJ7egPxKl(f{vnN=s0>Ey@B3DC(v8yZS)R$ z7kz*}Ltmn=(YNRaEWzPj1)12(!%Chol{@}b&z~$!3{O(8_=LlC4M!vBKza$yrTKIMt)!FaWSRr)lmt1~ynx^13x|r`Vb21P_+}@oE92(6 zBHnO$vnT8i3VIN#@CWOsP72Kux^cDcR<4Z$0^;M;h_8$bg*`r37_y~9$&M6 zVF*Zycg4Y=%hv)!ec>{$DbiBuo9l<}wVW5!k!$YmuAR!cp^wNwj2J(^Jhlr3s0gKX zphBwbK%*$juO@xE+uzJ(z!p#Rgfse%&8Xp8T%ky))*lJFxl-sCBQyq$;|YzWst#04 z2h*J6s0@y`995w4Xaax0O|B3(6Rttd3+r3JQA7vPL??UmT#uIvGt3p>{d&DGPZYO4k0 zi!FW%nuXG~p{b}A)uDRSfTp48Xa;ITGpU+tXcE=ZWU8ZjnnF{lVH=taQssm{7ivOo zxN^uwjUZVz__Nb=ngQ2Lc!tx^^=!3x6d57mlm!7vxv;U-zi_h0=V@~VoUFOuX8zPl z{Q%;w77#sSzkMow3p^nYY$wRvg;cz*c7G&Xz9`K3c(VX~tQS&o2mM~JAoLmjxpPAt z=;wu$LcMx5N=|Ev> zqE_*FA&P87i%>iNpPH#f_-|z`tWFdLik5=x^7_`^i72X;#g6=Xv^dlouxT9Niv^q`X+R8>1U66Q6kXOb?om7gX?`ysTQ--3r} zUI%)Fj-+X`0W+#VXKKSi;WRY;t6gmTF7zzYY(tNs$I%n$N%R!@2ilFEMtjgRw15`U zB07qWrkB#o=omVdj@yRzqJ4tiJdX~bgXjhHB4|x9EfMsll$OzQT0zIdQ`(HME7-z? zYs)4Aw2Jp-+yalAs{!Ph>kqaydP3!1PYW;LPFB;qM`dW52LOr-fvmCJWExySkBhf3 zeW*?HyQ4szDE91)ElxK2Qk=QM)#mZGJ6Uz_$tHv7dU+i<7l9F99HK$uD1eNGQU-3#I(mjn% zZlKVWx9Z>VbAT=And1!l9i?qSKcb(|&*&F)7X6BTqt&#APN7q2Ev=*VThJc>h37DW zW`IZ903bMx&VXhf7<1-IPXs}w7q)pc8Q&J5^quFS+*&RcS!-kKTT~*ubY=DzGPng#& z6>PNFAZ|D8Bbyp~RRL~bEr>bi>)xx%GH?O;;|#)4g0NMFdUfI-A;4$=LtI%|21d9t zG}+|=KZ&iXt?&dxVVEP}2ha?Hn^DSpk31MT*(7N0nW{&GpjU0U-OEjOh2{Z~ZdOI? z1_03a9#{unN`=Sk4T&s^e2xzW1WthE%H~DDu(;T&NLv#Zq!_$ODP?4GIgOKpyAw}| zYM+wIz_(S3*;3c+JM z&jA9^>a74#U18PmGUh$@XvFdSg2JLvqj|CG-*?}zfy$3vHfHR&{@kg))@y8Ww@)=E zYf-^mSVDe5L2g!l7Q~P$qys=029aU`M27*00GFXPU@8tmH1{6*1fsWd5U<&BHZH?W zxD7ABH{cub&G=S)do=#yqpy_^d9B9lAo5y|J0SMjjQ@_G!f)eWAcnG&0*IQL$vhH* zD2b1ZZX`F8%&Y@0v#TI14CTzwQY=t{JK%k3;XqYa9 zyG6WT$ikV_g0olUTHtAM0g{QYx*v zt$SpzZu|t@!+Gm2Iyd~F*itfaE=uddS+M&#w28WDa~IBo$9&4c<9wb~Q=fpMd!CGL ztp`41p-Xow2%eu?_aDa`16ZGqXW&NgEWyd;EmaMlp_$4B zf?No~I>Gs%0qU<-kw^O?{?K8WGdy9gP2d=g9KbMK>~@1&!!?5-a&tt-yT%^^$?*GT zi!lY)TnS@BTjK{N4%VicZO5cC`GC5BE8IE*P?K9U0Rr4kMS_#HUsUu}Uk4^O#4^Wt z+tD8r+@gR>h#&+ubBmw9>6Wf0&vYCWb8l z^?%{29y1tC7t6uZ!7cXy2Nr9mLeG+8U>PrVWY@QMFA-M1|A^1mIXta%#E5QJzB4_; zI&QuQG}dv;hYEKcSujy{-bS51C-*OTzYyM8xqzm6$h}<#QA8btsB<9GHV?9Bt09B7 z0qsKjA!l|NGG(tre(Y=X8;*l)*g!lIw5XzOdKYzFP~%=Lgb6I1La+zc)>cGF-7&H~^A@Uiy~djhIuK!_9(5*gNUfpNm7 z#3)^fy*#CPG}3|F=mMUSIRcrHnjklq3kJF7QqbQPf3Uqf{;n(&gIhdb@W8vpB}OrV zm!h3NOZggM+*pUxO)ar&rO%^lx+ty_#M_DP2mhrPn=#uSITrJ=oD@ zU`cNRTY3xJ-$t*8%)~N!BfW{3R3b8N1crq}N0vX^Ot8;gjJZ$bFoL#GyHpU5h+z zo~yX#DD0|)(Jf$rcoun~K91MmwSepE&^mlS7$Wdtd0^#tNDTY|CVRbWaCsvfi|Gxb zwyeh+z-R3q(TO`yb?+CA)d9{|=>ump#3JzJqPsDPn;Wk4PxZ94hK1-1Z-&4QZ=tu) zi7^q`hIgX0%^(~Pr)qX%~JBexHE2>`r8tF-|K)~S>ytyc) z^ZTp%wDR-JjBx;&Nf;e_>OqEXprQ}chadvEk8X!w6;-~8Kf)j5PhfGM;?MBs_zQdz ze~G_BZrH4ts~^j6%;~{9QI*x*p4(KvKMej6C%%V~Z4i+4bca0>w!3<6jAl<8-9%T@ zEp!`QMKhdiW;a;&*-VjOkEhrTo8obO1KB$;Ip5)bHM8_F?6slKp}IgdhtLpvM4-`7B{{jms14#OHW3^dI{~c>w$8 z#%JkzkWHBKFPw~6-*_U0z8$oa-XC*JSQ2X-AQ0Lp61q75>o`#%B}qgR$Y4~_TU2I* zJ#C<0uC@S8q?;+2dq^pgB%%e)1ydP2tJwK>l4O3p{WXD)O7tX!q&5Zs!o+=|57Mn* zd;c%e`C^(r(3ZJ~&-#jNn}~TPFddV^PGSL2?DazIWg~Vm5!6S#dXBiSu5}UzzhXXd zbW#0ba{`ejug5Jim>v?!BNxh;$jfjt0zwm@-3KLk?C2zE{Jj0P2@+2-0J=#g-2sU< zk^`HcOY$J>a7Vx~h}C&|K_7jT9_<&%NFgc0?~&0wkO|PRhwcPuIC23rkg;T33>xUu z^zSi^6}hPZ{UhVa1X4*Zr@QFm^hx@UZv7*ZdHti0^{;L*xt=P>^%EDRAkzc|d8)U9 z#K_DN$n5?ftB0rvQ9DC85`cORnM+zoEAfykAu-@3ZNx|X^cngr-Anh;=jeX=JUu`U z(igUo`9faWNg`rBhqNObeNjkT+UQHsdd@%L8SK;l*Kh5hH^pm1TwLt zQ>fC!X52)!3iTJV1?n%a{q90Wx_Lf@qKv+*&Fd`ZyAw|I@5(Vb?DHU3k8hIjrmG#nQw zymwL1@Rk4#|LPAK-WSMx_}_tscZiC7OglF#5oKZkhs3vv=oz`v2N;5(CbV=9O_|Z^Xff7F;PH3uYp%6|%6sYUl8vSab*6j~KBGA;QQ5fzI!en zdX}ca2-;^v2~&#FAR!8$fCxosrh=K!y~axVd$hjDOybuwnW>_GEXAm2K~tFe^Cx5) zV8ZG2Pnhr=ly|#9&^pd@MW3l>Gp-B9HX$d|ycA<rz-8k50f!M9L(@EuekQ_XmpcIHl|liAB0Wxi%kGrux_Fz4b( zTwGjyTz*_>Tt(c3xQTI-Cdn<5+a${+cS`P-tdQ)K9Fd%f*Tj#Czaswn z_^$Z<@gKyWj6W5BI{w@EU!+*dNF`FKR4z@B4wNdTTB%8DmpY`m(tK&5bhPv`=~(G3 zsYmLQ2BbmhLTS5nv2=;_Ht8zqTIojVCg~RGqta)j`=zf)4@=*Xejxow`ibq-#H8PKEf$UbS0 z{4M$0@{i@GPQLUJ&s8cj3 z+=`$gtXQB}q_|4)H^tQos<=aOm*O79O2vJO)rz%>`xWaIk0?$gkc83%E@5fHx`Zba z_9YxrUaB0U9Ivcb&QiLS0p&HyrONA+Hz;pZ-mJV;dAo9*@&V-rWv6nZa$(?>_FDUX0W5#61I%3U?;HE>=d?^t!JmPSFy|44eWOI3HB*=H@k;@mVJ?Z ziG77V%)Z7RWlyrdtJJD2)n%$NsxvAeS;Sc z-Y|H};3Mjh>JoLidc68_^(1wbdV%^H^=;}q)OV>@sP9#;Qtwe8RKK7;q&}+tm-ja-wUv1n`>hi0f|xF$`Lp~=$ZX!11qnnKMe&1}srnsu5j znwK>nC&`iwNySMuNzF;~lWs^_m9!>lUDBqcok>q5?N2(K^jgxVNvD&(OZq)) zr;<w&=1pR=qKwN^|SPI^jGNH^~>}(>2J~BreC4ISKp=Iq~D@{P=8eaFa1~g zGy0$OzvzEWVN#M)3@Ldjm!(Wdxjbc3%FdMCDKDhFl5!&D!_ZTgVkU+3^5Edj4-4d z@(p7R#fDNtxnaDa($HX-ZfG>jGWZOO4NDBy7?v8=7`7N5G(2S3Zg||V-*CY2g5i+i zJHsD_b4F}rjI7aagvz{egfZQiY0Ng}8b=xnj77#OV~ugDvCi0FoNjD1&N9w1USVu9 zHXD~3Hy95XKQ}3$+FWM}neI02Fg;~@+VqTRuj!EKsOh-r4butJ+opF-CrzhJr%m6P zzBipQtIP(o)oeEpF{ha`%vt6fbFuky^F;GxbEDa5cA4GgR`Zo+ui0l_VqR_TH19U= zHJ>nlX(5(4OT0y9QCJ38F0mw8Y!-)QsAae%&5~isvgBCCT8b^DmU7E@OP!^`GTqW> zX|~L_ge(!uLd(sTdo8OhYb@(5k6ZRz4p?5W9I_m-yl?ry@{#2eYl1b;I?;NCHEg}n zy2iT0`n>g^^+oGT)>o{Dt*=?%vVLU!%|>h~wjA47+XUO?wn?@sTa9goZKiFu&1rMl z+%~Umfo-X6wQaL)hi#{Am+f)eleT@f{k8+P7i@=YFWXMqCHA5AVtbW+ioMp}V4rSp zwEOIf?N{5W{W|*%_8aYM?Hlb6+8?rSx9_m;u|ID=Xn)cElKqJNnEiG8oA$TtpWA|*F6gr9>RgP*$jbp0A>1c8^J6aqb$2>>a(e7C6xY|J- zw>wri?scqktZ{TWx*VGvTO1EMb~*MqUUD399CN(xc+>H|<3q>Cj?WxlIL?YjjS%t5 Lw2If*?-24|EPDPr literal 0 HcmV?d00001 diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme new file mode 100644 index 0000000..c802e05 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/ComposableArchitecture.xcscheme @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/reactiveswift-composable-architecture-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/reactiveswift-composable-architecture-Package.xcscheme new file mode 100644 index 0000000..6187657 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/reactiveswift-composable-architecture-Package.xcscheme @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme new file mode 100644 index 0000000..1e2feec --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/swift-composable-architecture-benchmark.xcscheme @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist b/.swiftpm/xcode/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..8fff740 --- /dev/null +++ b/.swiftpm/xcode/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,87 @@ + + + + + SchemeUserState + + ComposableArchitecture.xcscheme_^#shared#^_ + + orderHint + 4 + + CustomDump (Playground) 1.xcscheme + + isShown + + orderHint + 18 + + CustomDump (Playground) 2.xcscheme + + isShown + + orderHint + 19 + + CustomDump (Playground).xcscheme + + isShown + + orderHint + 17 + + ReactiveSwift (Playground) 1.xcscheme + + isShown + + orderHint + 24 + + ReactiveSwift (Playground) 2.xcscheme + + isShown + + orderHint + 25 + + ReactiveSwift (Playground).xcscheme + + isShown + + orderHint + 23 + + ReactiveSwift-UIExamples (Playground) 1.xcscheme + + isShown + + orderHint + 21 + + ReactiveSwift-UIExamples (Playground) 2.xcscheme + + isShown + + orderHint + 22 + + ReactiveSwift-UIExamples (Playground).xcscheme + + isShown + + orderHint + 20 + + reactiveswift-composable-architecture-Package.xcscheme_^#shared#^_ + + orderHint + 9 + + swift-composable-architecture-benchmark.xcscheme_^#shared#^_ + + orderHint + 13 + + + + diff --git a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1eef849 --- /dev/null +++ b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved new file mode 100644 index 0000000..cf43ed3 --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -0,0 +1,124 @@ +{ + "object": { + "pins": [ + { + "package": "Alamofire", + "repositoryURL": "https://github.com/Alamofire/Alamofire.git", + "state": { + "branch": null, + "revision": "d120af1e8638c7da36c8481fd61a66c0c08dc4fc", + "version": "5.4.4" + } + }, + { + "package": "AnyRequest", + "repositoryURL": "https://github.com/FullStack-Swift/AnyRequest", + "state": { + "branch": "main", + "revision": "42a2e613f0d3f3e7390d9817262c28533804e97d", + "version": null + } + }, + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", + "version": "0.5.3" + } + }, + { + "package": "Json", + "repositoryURL": "https://github.com/FullStack-Swift/Json", + "state": { + "branch": "main", + "revision": "1b6b4508691d0341fc3a5ddd7a75845734bdf41d", + "version": null + } + }, + { + "package": "ReactiveCocoa", + "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveCocoa.git", + "state": { + "branch": null, + "revision": "579364393ad587352bcce133b7b3f73392cb585b", + "version": "11.2.2" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", + "version": "0.5.0" + } + }, + { + "package": "Benchmark", + "repositoryURL": "https://github.com/google/swift-benchmark", + "state": { + "branch": null, + "revision": "a0564bf88df5f94eec81348a2f089494c6b28d80", + "version": "0.1.1" + } + }, + { + "package": "swift-case-paths", + "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", + "state": { + "branch": null, + "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", + "version": "0.7.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", + "version": "1.0.1" + } + }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "c2dd2c64b753dda592f5619303e02f741cd3e862", + "version": "0.2.0" + } + }, + { + "package": "swift-identified-collections", + "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", + "state": { + "branch": null, + "revision": "c8e6a40209650ab619853cd4ce89a0aa51792754", + "version": "0.3.0" + } + }, + { + "package": "ConvertSwift", + "repositoryURL": "https://github.com/FullStack-Swift/SwiftConvert", + "state": { + "branch": "main", + "revision": "46c1624cff8a1df05edb503a5b9940bf27beabce", + "version": null + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", + "version": "0.2.1" + } + } + ] + }, + "version": 1 +} diff --git a/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..793131d51d418e0f67b3abe3d12957b14b0de934 GIT binary patch literal 272511 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 literal 0 HcmV?d00001 diff --git a/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist new file mode 100644 index 0000000..9e6f25a --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -0,0 +1,6 @@ + + + diff --git a/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist new file mode 100644 index 0000000..a1c854e --- /dev/null +++ b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/xcschemes/xcschememanagement.plist @@ -0,0 +1,261 @@ + + + + + SchemeUserState + + CustomDump (Playground) 1.xcscheme + + isShown + + orderHint + 22 + + CustomDump (Playground) 2.xcscheme + + isShown + + orderHint + 23 + + CustomDump (Playground).xcscheme + + isShown + + orderHint + 21 + + ReactiveCocoa-iOS (Playground) 1.xcscheme + + isShown + + orderHint + 37 + + ReactiveCocoa-iOS (Playground) 2.xcscheme + + isShown + + orderHint + 38 + + ReactiveCocoa-iOS (Playground).xcscheme + + isShown + + orderHint + 36 + + ReactiveCocoa-macOS (Playground) 1.xcscheme + + isShown + + orderHint + 34 + + ReactiveCocoa-macOS (Playground) 2.xcscheme + + isShown + + orderHint + 35 + + ReactiveCocoa-macOS (Playground).xcscheme + + isShown + + orderHint + 33 + + ReactiveCocoa-tvOS (Playground) 1.xcscheme + + isShown + + orderHint + 31 + + ReactiveCocoa-tvOS (Playground) 2.xcscheme + + isShown + + orderHint + 32 + + ReactiveCocoa-tvOS (Playground).xcscheme + + isShown + + orderHint + 30 + + ReactiveSwift (Playground) 1.xcscheme + + isShown + + orderHint + 25 + + ReactiveSwift (Playground) 10.xcscheme + + isShown + + orderHint + 52 + + ReactiveSwift (Playground) 11.xcscheme + + isShown + + orderHint + 53 + + ReactiveSwift (Playground) 2.xcscheme + + isShown + + orderHint + 26 + + ReactiveSwift (Playground) 3.xcscheme + + isShown + + orderHint + 39 + + ReactiveSwift (Playground) 4.xcscheme + + isShown + + orderHint + 40 + + ReactiveSwift (Playground) 5.xcscheme + + isShown + + orderHint + 41 + + ReactiveSwift (Playground) 6.xcscheme + + isShown + + orderHint + 45 + + ReactiveSwift (Playground) 7.xcscheme + + isShown + + orderHint + 46 + + ReactiveSwift (Playground) 8.xcscheme + + isShown + + orderHint + 47 + + ReactiveSwift (Playground) 9.xcscheme + + isShown + + orderHint + 51 + + ReactiveSwift (Playground).xcscheme + + isShown + + orderHint + 24 + + ReactiveSwift-UIExamples (Playground) 1.xcscheme + + isShown + + orderHint + 28 + + ReactiveSwift-UIExamples (Playground) 10.xcscheme + + isShown + + orderHint + 55 + + ReactiveSwift-UIExamples (Playground) 11.xcscheme + + isShown + + orderHint + 56 + + ReactiveSwift-UIExamples (Playground) 2.xcscheme + + isShown + + orderHint + 29 + + ReactiveSwift-UIExamples (Playground) 3.xcscheme + + isShown + + orderHint + 42 + + ReactiveSwift-UIExamples (Playground) 4.xcscheme + + isShown + + orderHint + 43 + + ReactiveSwift-UIExamples (Playground) 5.xcscheme + + isShown + + orderHint + 44 + + ReactiveSwift-UIExamples (Playground) 6.xcscheme + + isShown + + orderHint + 48 + + ReactiveSwift-UIExamples (Playground) 7.xcscheme + + isShown + + orderHint + 49 + + ReactiveSwift-UIExamples (Playground) 8.xcscheme + + isShown + + orderHint + 50 + + ReactiveSwift-UIExamples (Playground) 9.xcscheme + + isShown + + orderHint + 54 + + ReactiveSwift-UIExamples (Playground).xcscheme + + isShown + + orderHint + 27 + + + + diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj new file mode 100644 index 0000000..19e818e --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj @@ -0,0 +1,772 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 87030D732710EF07009A5353 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 87030D722710EF07009A5353 /* ComposableArchitecture */; }; + 87030D772710EFF5009A5353 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 87030D762710EFF5009A5353 /* ComposableArchitecture */; }; + 873FE73427062BB80054CF07 /* RootAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE72F27062BB80054CF07 /* RootAction.swift */; }; + 873FE73527062BB80054CF07 /* RootViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73027062BB80054CF07 /* RootViewController.swift */; }; + 873FE73627062BB80054CF07 /* RootEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73127062BB80054CF07 /* RootEnvironment.swift */; }; + 873FE73727062BB80054CF07 /* RootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73227062BB80054CF07 /* RootReducer.swift */; }; + 873FE73827062BB80054CF07 /* RootState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73327062BB80054CF07 /* RootState.swift */; }; + 873FE73B27062C7A0054CF07 /* ActivityIndicatorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73A27062C7A0054CF07 /* ActivityIndicatorViewController.swift */; }; + 873FE73D27062C8B0054CF07 /* IfLetStoreController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73C27062C8B0054CF07 /* IfLetStoreController.swift */; }; + 873FE74327062D850054CF07 /* AuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73E27062D850054CF07 /* AuthAction.swift */; }; + 873FE74427062D850054CF07 /* AuthViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE73F27062D850054CF07 /* AuthViewController.swift */; }; + 873FE74527062D850054CF07 /* AuthEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74027062D850054CF07 /* AuthEnvironment.swift */; }; + 873FE74627062D850054CF07 /* AuthReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74127062D850054CF07 /* AuthReducer.swift */; }; + 873FE74727062D850054CF07 /* AuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74227062D850054CF07 /* AuthState.swift */; }; + 873FE74E27062DF80054CF07 /* MainAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74927062DF70054CF07 /* MainAction.swift */; }; + 873FE74F27062DF80054CF07 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74A27062DF70054CF07 /* MainViewController.swift */; }; + 873FE75027062DF80054CF07 /* MainEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74B27062DF70054CF07 /* MainEnvironment.swift */; }; + 873FE75127062DF80054CF07 /* MainReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74C27062DF70054CF07 /* MainReducer.swift */; }; + 873FE75227062DF80054CF07 /* MainState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE74D27062DF70054CF07 /* MainState.swift */; }; + 873FE759270632E00054CF07 /* CountersTableAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE754270632E00054CF07 /* CountersTableAction.swift */; }; + 873FE75A270632E00054CF07 /* CountersTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE755270632E00054CF07 /* CountersTableViewController.swift */; }; + 873FE75B270632E00054CF07 /* CountersTableEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE756270632E00054CF07 /* CountersTableEnvironment.swift */; }; + 873FE75C270632E00054CF07 /* CountersTableReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE757270632E00054CF07 /* CountersTableReducer.swift */; }; + 873FE75D270632E00054CF07 /* CountersTableState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE758270632E00054CF07 /* CountersTableState.swift */; }; + 873FE765270633310054CF07 /* LazyNavigationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE760270633310054CF07 /* LazyNavigationAction.swift */; }; + 873FE766270633310054CF07 /* LazyNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE761270633310054CF07 /* LazyNavigationViewController.swift */; }; + 873FE767270633310054CF07 /* LazyNavigationEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE762270633310054CF07 /* LazyNavigationEnvironment.swift */; }; + 873FE768270633310054CF07 /* LazyNavigationReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE763270633310054CF07 /* LazyNavigationReducer.swift */; }; + 873FE769270633310054CF07 /* LazyNavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE764270633310054CF07 /* LazyNavigationState.swift */; }; + 873FE76F2706334C0054CF07 /* EagerNavigationAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE76A2706334C0054CF07 /* EagerNavigationAction.swift */; }; + 873FE7702706334C0054CF07 /* EagerNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE76B2706334C0054CF07 /* EagerNavigationViewController.swift */; }; + 873FE7712706334C0054CF07 /* EagerNavigationEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE76C2706334C0054CF07 /* EagerNavigationEnvironment.swift */; }; + 873FE7722706334C0054CF07 /* EagerNavigationReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE76D2706334C0054CF07 /* EagerNavigationReducer.swift */; }; + 873FE7732706334C0054CF07 /* EagerNavigationState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 873FE76E2706334C0054CF07 /* EagerNavigationState.swift */; }; + 878AF92D2710F0D30066E71C /* ReactiveCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 878AF92C2710F0D30066E71C /* ReactiveCocoa */; }; + 87BA5F3F270622DB00984D7B /* SwiftUICaseStudiesApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F3E270622DB00984D7B /* SwiftUICaseStudiesApp.swift */; }; + 87BA5F41270622DB00984D7B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F40270622DB00984D7B /* ContentView.swift */; }; + 87BA5F43270622E500984D7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87BA5F42270622E500984D7B /* Assets.xcassets */; }; + 87BA5F46270622E500984D7B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87BA5F45270622E500984D7B /* Preview Assets.xcassets */; }; + 87BA5F512706231100984D7B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F502706231100984D7B /* AppDelegate.swift */; }; + 87BA5F532706231100984D7B /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F522706231100984D7B /* SceneDelegate.swift */; }; + 87BA5F582706231100984D7B /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87BA5F562706231100984D7B /* Main.storyboard */; }; + 87BA5F5A2706231200984D7B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 87BA5F592706231200984D7B /* Assets.xcassets */; }; + 87BA5F5D2706231200984D7B /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 87BA5F5B2706231200984D7B /* LaunchScreen.storyboard */; }; + 87BA5F682706240E00984D7B /* CounterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F632706240E00984D7B /* CounterAction.swift */; }; + 87BA5F692706240E00984D7B /* CounterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F642706240E00984D7B /* CounterViewController.swift */; }; + 87BA5F6A2706240E00984D7B /* CounterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F652706240E00984D7B /* CounterEnvironment.swift */; }; + 87BA5F6B2706240E00984D7B /* CounterReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F662706240E00984D7B /* CounterReducer.swift */; }; + 87BA5F6C2706240E00984D7B /* CounterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87BA5F672706240E00984D7B /* CounterState.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 873FE72F27062BB80054CF07 /* RootAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootAction.swift; sourceTree = ""; }; + 873FE73027062BB80054CF07 /* RootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewController.swift; sourceTree = ""; }; + 873FE73127062BB80054CF07 /* RootEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootEnvironment.swift; sourceTree = ""; }; + 873FE73227062BB80054CF07 /* RootReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootReducer.swift; sourceTree = ""; }; + 873FE73327062BB80054CF07 /* RootState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootState.swift; sourceTree = ""; }; + 873FE73A27062C7A0054CF07 /* ActivityIndicatorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicatorViewController.swift; sourceTree = ""; }; + 873FE73C27062C8B0054CF07 /* IfLetStoreController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IfLetStoreController.swift; sourceTree = ""; }; + 873FE73E27062D850054CF07 /* AuthAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthAction.swift; sourceTree = ""; }; + 873FE73F27062D850054CF07 /* AuthViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewController.swift; sourceTree = ""; }; + 873FE74027062D850054CF07 /* AuthEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthEnvironment.swift; sourceTree = ""; }; + 873FE74127062D850054CF07 /* AuthReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthReducer.swift; sourceTree = ""; }; + 873FE74227062D850054CF07 /* AuthState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthState.swift; sourceTree = ""; }; + 873FE74927062DF70054CF07 /* MainAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainAction.swift; sourceTree = ""; }; + 873FE74A27062DF70054CF07 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + 873FE74B27062DF70054CF07 /* MainEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainEnvironment.swift; sourceTree = ""; }; + 873FE74C27062DF70054CF07 /* MainReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainReducer.swift; sourceTree = ""; }; + 873FE74D27062DF70054CF07 /* MainState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainState.swift; sourceTree = ""; }; + 873FE754270632E00054CF07 /* CountersTableAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountersTableAction.swift; sourceTree = ""; }; + 873FE755270632E00054CF07 /* CountersTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountersTableViewController.swift; sourceTree = ""; }; + 873FE756270632E00054CF07 /* CountersTableEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountersTableEnvironment.swift; sourceTree = ""; }; + 873FE757270632E00054CF07 /* CountersTableReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountersTableReducer.swift; sourceTree = ""; }; + 873FE758270632E00054CF07 /* CountersTableState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CountersTableState.swift; sourceTree = ""; }; + 873FE760270633310054CF07 /* LazyNavigationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyNavigationAction.swift; sourceTree = ""; }; + 873FE761270633310054CF07 /* LazyNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyNavigationViewController.swift; sourceTree = ""; }; + 873FE762270633310054CF07 /* LazyNavigationEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyNavigationEnvironment.swift; sourceTree = ""; }; + 873FE763270633310054CF07 /* LazyNavigationReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyNavigationReducer.swift; sourceTree = ""; }; + 873FE764270633310054CF07 /* LazyNavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyNavigationState.swift; sourceTree = ""; }; + 873FE76A2706334C0054CF07 /* EagerNavigationAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EagerNavigationAction.swift; sourceTree = ""; }; + 873FE76B2706334C0054CF07 /* EagerNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EagerNavigationViewController.swift; sourceTree = ""; }; + 873FE76C2706334C0054CF07 /* EagerNavigationEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EagerNavigationEnvironment.swift; sourceTree = ""; }; + 873FE76D2706334C0054CF07 /* EagerNavigationReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EagerNavigationReducer.swift; sourceTree = ""; }; + 873FE76E2706334C0054CF07 /* EagerNavigationState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EagerNavigationState.swift; sourceTree = ""; }; + 87BA5F3C270622DB00984D7B /* SwiftUICaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUICaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 87BA5F3E270622DB00984D7B /* SwiftUICaseStudiesApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUICaseStudiesApp.swift; sourceTree = ""; }; + 87BA5F40270622DB00984D7B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 87BA5F42270622E500984D7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 87BA5F45270622E500984D7B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 87BA5F4E2706231100984D7B /* UIKitCaseStudies.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UIKitCaseStudies.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 87BA5F502706231100984D7B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 87BA5F522706231100984D7B /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 87BA5F572706231100984D7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 87BA5F592706231200984D7B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 87BA5F5C2706231200984D7B /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 87BA5F5E2706231200984D7B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 87BA5F632706240E00984D7B /* CounterAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterAction.swift; sourceTree = ""; }; + 87BA5F642706240E00984D7B /* CounterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterViewController.swift; sourceTree = ""; }; + 87BA5F652706240E00984D7B /* CounterEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterEnvironment.swift; sourceTree = ""; }; + 87BA5F662706240E00984D7B /* CounterReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterReducer.swift; sourceTree = ""; }; + 87BA5F672706240E00984D7B /* CounterState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterState.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 87BA5F39270622DB00984D7B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 87030D732710EF07009A5353 /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87BA5F4B2706231100984D7B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 87030D772710EFF5009A5353 /* ComposableArchitecture in Frameworks */, + 878AF92D2710F0D30066E71C /* ReactiveCocoa in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 872458582706058700A88871 = { + isa = PBXGroup; + children = ( + 87BA5F3D270622DB00984D7B /* SwiftUICaseStudies */, + 87BA5F4F2706231100984D7B /* UIKitCaseStudies */, + 872458662706058800A88871 /* Products */, + 87DAAA6027060D190048D12A /* Frameworks */, + ); + sourceTree = ""; + }; + 872458662706058800A88871 /* Products */ = { + isa = PBXGroup; + children = ( + 87BA5F3C270622DB00984D7B /* SwiftUICaseStudies.app */, + 87BA5F4E2706231100984D7B /* UIKitCaseStudies.app */, + ); + name = Products; + sourceTree = ""; + }; + 873FE72D27062B9A0054CF07 /* RootScreen */ = { + isa = PBXGroup; + children = ( + 873FE73027062BB80054CF07 /* RootViewController.swift */, + 873FE73327062BB80054CF07 /* RootState.swift */, + 873FE72F27062BB80054CF07 /* RootAction.swift */, + 873FE73127062BB80054CF07 /* RootEnvironment.swift */, + 873FE73227062BB80054CF07 /* RootReducer.swift */, + ); + path = RootScreen; + sourceTree = ""; + }; + 873FE72E27062BA20054CF07 /* AuthScreen */ = { + isa = PBXGroup; + children = ( + 873FE73F27062D850054CF07 /* AuthViewController.swift */, + 873FE74227062D850054CF07 /* AuthState.swift */, + 873FE73E27062D850054CF07 /* AuthAction.swift */, + 873FE74027062D850054CF07 /* AuthEnvironment.swift */, + 873FE74127062D850054CF07 /* AuthReducer.swift */, + ); + path = AuthScreen; + sourceTree = ""; + }; + 873FE73927062C690054CF07 /* Internal */ = { + isa = PBXGroup; + children = ( + 873FE73A27062C7A0054CF07 /* ActivityIndicatorViewController.swift */, + 873FE73C27062C8B0054CF07 /* IfLetStoreController.swift */, + ); + path = Internal; + sourceTree = ""; + }; + 873FE74827062DE80054CF07 /* MainScreen */ = { + isa = PBXGroup; + children = ( + 873FE74A27062DF70054CF07 /* MainViewController.swift */, + 873FE74D27062DF70054CF07 /* MainState.swift */, + 873FE74927062DF70054CF07 /* MainAction.swift */, + 873FE74B27062DF70054CF07 /* MainEnvironment.swift */, + 873FE74C27062DF70054CF07 /* MainReducer.swift */, + ); + path = MainScreen; + sourceTree = ""; + }; + 873FE753270632BC0054CF07 /* CountersListScreen */ = { + isa = PBXGroup; + children = ( + 873FE755270632E00054CF07 /* CountersTableViewController.swift */, + 873FE758270632E00054CF07 /* CountersTableState.swift */, + 873FE754270632E00054CF07 /* CountersTableAction.swift */, + 873FE756270632E00054CF07 /* CountersTableEnvironment.swift */, + 873FE757270632E00054CF07 /* CountersTableReducer.swift */, + ); + path = CountersListScreen; + sourceTree = ""; + }; + 873FE75E270632ED0054CF07 /* EagerNavigation */ = { + isa = PBXGroup; + children = ( + 873FE76B2706334C0054CF07 /* EagerNavigationViewController.swift */, + 873FE76E2706334C0054CF07 /* EagerNavigationState.swift */, + 873FE76A2706334C0054CF07 /* EagerNavigationAction.swift */, + 873FE76C2706334C0054CF07 /* EagerNavigationEnvironment.swift */, + 873FE76D2706334C0054CF07 /* EagerNavigationReducer.swift */, + ); + path = EagerNavigation; + sourceTree = ""; + }; + 873FE75F270633070054CF07 /* LazyNavigation */ = { + isa = PBXGroup; + children = ( + 873FE761270633310054CF07 /* LazyNavigationViewController.swift */, + 873FE764270633310054CF07 /* LazyNavigationState.swift */, + 873FE760270633310054CF07 /* LazyNavigationAction.swift */, + 873FE762270633310054CF07 /* LazyNavigationEnvironment.swift */, + 873FE763270633310054CF07 /* LazyNavigationReducer.swift */, + ); + path = LazyNavigation; + sourceTree = ""; + }; + 87BA5F3D270622DB00984D7B /* SwiftUICaseStudies */ = { + isa = PBXGroup; + children = ( + 87BA5F3E270622DB00984D7B /* SwiftUICaseStudiesApp.swift */, + 87BA5F40270622DB00984D7B /* ContentView.swift */, + 87BA5F42270622E500984D7B /* Assets.xcassets */, + 87BA5F44270622E500984D7B /* Preview Content */, + ); + path = SwiftUICaseStudies; + sourceTree = ""; + }; + 87BA5F44270622E500984D7B /* Preview Content */ = { + isa = PBXGroup; + children = ( + 87BA5F45270622E500984D7B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 87BA5F4F2706231100984D7B /* UIKitCaseStudies */ = { + isa = PBXGroup; + children = ( + 873FE73927062C690054CF07 /* Internal */, + 873FE72D27062B9A0054CF07 /* RootScreen */, + 873FE74827062DE80054CF07 /* MainScreen */, + 873FE72E27062BA20054CF07 /* AuthScreen */, + 87BA5F62270623FC00984D7B /* CounterScreen */, + 873FE753270632BC0054CF07 /* CountersListScreen */, + 873FE75E270632ED0054CF07 /* EagerNavigation */, + 873FE75F270633070054CF07 /* LazyNavigation */, + 87BA5F502706231100984D7B /* AppDelegate.swift */, + 87BA5F522706231100984D7B /* SceneDelegate.swift */, + 87BA5F562706231100984D7B /* Main.storyboard */, + 87BA5F592706231200984D7B /* Assets.xcassets */, + 87BA5F5B2706231200984D7B /* LaunchScreen.storyboard */, + 87BA5F5E2706231200984D7B /* Info.plist */, + ); + path = UIKitCaseStudies; + sourceTree = ""; + }; + 87BA5F62270623FC00984D7B /* CounterScreen */ = { + isa = PBXGroup; + children = ( + 87BA5F642706240E00984D7B /* CounterViewController.swift */, + 87BA5F672706240E00984D7B /* CounterState.swift */, + 87BA5F632706240E00984D7B /* CounterAction.swift */, + 87BA5F652706240E00984D7B /* CounterEnvironment.swift */, + 87BA5F662706240E00984D7B /* CounterReducer.swift */, + ); + path = CounterScreen; + sourceTree = ""; + }; + 87DAAA6027060D190048D12A /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 87BA5F3B270622DB00984D7B /* SwiftUICaseStudies */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87BA5F49270622E500984D7B /* Build configuration list for PBXNativeTarget "SwiftUICaseStudies" */; + buildPhases = ( + 87BA5F38270622DB00984D7B /* Sources */, + 87BA5F39270622DB00984D7B /* Frameworks */, + 87BA5F3A270622DB00984D7B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SwiftUICaseStudies; + packageProductDependencies = ( + 87030D722710EF07009A5353 /* ComposableArchitecture */, + ); + productName = SwiftUICaseStudies; + productReference = 87BA5F3C270622DB00984D7B /* SwiftUICaseStudies.app */; + productType = "com.apple.product-type.application"; + }; + 87BA5F4D2706231100984D7B /* UIKitCaseStudies */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87BA5F5F2706231200984D7B /* Build configuration list for PBXNativeTarget "UIKitCaseStudies" */; + buildPhases = ( + 87BA5F4A2706231100984D7B /* Sources */, + 87BA5F4B2706231100984D7B /* Frameworks */, + 87BA5F4C2706231100984D7B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UIKitCaseStudies; + packageProductDependencies = ( + 87030D762710EFF5009A5353 /* ComposableArchitecture */, + 878AF92C2710F0D30066E71C /* ReactiveCocoa */, + ); + productName = UIKitCaseStudies; + productReference = 87BA5F4E2706231100984D7B /* UIKitCaseStudies.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 872458592706058700A88871 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + TargetAttributes = { + 87BA5F3B270622DB00984D7B = { + CreatedOnToolsVersion = 13.0; + }; + 87BA5F4D2706231100984D7B = { + CreatedOnToolsVersion = 13.0; + }; + }; + }; + buildConfigurationList = 8724585C2706058700A88871 /* Build configuration list for PBXProject "CaseStudies" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 872458582706058700A88871; + packageReferences = ( + 87030D6F2710EED7009A5353 /* XCRemoteSwiftPackageReference "ReactiveCocoa" */, + ); + productRefGroup = 872458662706058800A88871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 87BA5F3B270622DB00984D7B /* SwiftUICaseStudies */, + 87BA5F4D2706231100984D7B /* UIKitCaseStudies */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 87BA5F3A270622DB00984D7B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 87BA5F46270622E500984D7B /* Preview Assets.xcassets in Resources */, + 87BA5F43270622E500984D7B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87BA5F4C2706231100984D7B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 87BA5F5D2706231200984D7B /* LaunchScreen.storyboard in Resources */, + 87BA5F5A2706231200984D7B /* Assets.xcassets in Resources */, + 87BA5F582706231100984D7B /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 87BA5F38270622DB00984D7B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 87BA5F41270622DB00984D7B /* ContentView.swift in Sources */, + 87BA5F3F270622DB00984D7B /* SwiftUICaseStudiesApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87BA5F4A2706231100984D7B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 873FE74F27062DF80054CF07 /* MainViewController.swift in Sources */, + 873FE765270633310054CF07 /* LazyNavigationAction.swift in Sources */, + 873FE759270632E00054CF07 /* CountersTableAction.swift in Sources */, + 87BA5F692706240E00984D7B /* CounterViewController.swift in Sources */, + 87BA5F682706240E00984D7B /* CounterAction.swift in Sources */, + 873FE7712706334C0054CF07 /* EagerNavigationEnvironment.swift in Sources */, + 873FE7702706334C0054CF07 /* EagerNavigationViewController.swift in Sources */, + 873FE74E27062DF80054CF07 /* MainAction.swift in Sources */, + 873FE73527062BB80054CF07 /* RootViewController.swift in Sources */, + 873FE769270633310054CF07 /* LazyNavigationState.swift in Sources */, + 873FE74727062D850054CF07 /* AuthState.swift in Sources */, + 873FE73627062BB80054CF07 /* RootEnvironment.swift in Sources */, + 873FE74427062D850054CF07 /* AuthViewController.swift in Sources */, + 873FE74327062D850054CF07 /* AuthAction.swift in Sources */, + 873FE75A270632E00054CF07 /* CountersTableViewController.swift in Sources */, + 873FE7732706334C0054CF07 /* EagerNavigationState.swift in Sources */, + 87BA5F6A2706240E00984D7B /* CounterEnvironment.swift in Sources */, + 873FE73B27062C7A0054CF07 /* ActivityIndicatorViewController.swift in Sources */, + 87BA5F512706231100984D7B /* AppDelegate.swift in Sources */, + 873FE766270633310054CF07 /* LazyNavigationViewController.swift in Sources */, + 873FE7722706334C0054CF07 /* EagerNavigationReducer.swift in Sources */, + 87BA5F6B2706240E00984D7B /* CounterReducer.swift in Sources */, + 873FE76F2706334C0054CF07 /* EagerNavigationAction.swift in Sources */, + 873FE73D27062C8B0054CF07 /* IfLetStoreController.swift in Sources */, + 873FE75127062DF80054CF07 /* MainReducer.swift in Sources */, + 873FE75027062DF80054CF07 /* MainEnvironment.swift in Sources */, + 873FE768270633310054CF07 /* LazyNavigationReducer.swift in Sources */, + 873FE75227062DF80054CF07 /* MainState.swift in Sources */, + 873FE74527062D850054CF07 /* AuthEnvironment.swift in Sources */, + 873FE75D270632E00054CF07 /* CountersTableState.swift in Sources */, + 873FE74627062D850054CF07 /* AuthReducer.swift in Sources */, + 873FE75B270632E00054CF07 /* CountersTableEnvironment.swift in Sources */, + 873FE75C270632E00054CF07 /* CountersTableReducer.swift in Sources */, + 873FE73827062BB80054CF07 /* RootState.swift in Sources */, + 873FE73427062BB80054CF07 /* RootAction.swift in Sources */, + 87BA5F6C2706240E00984D7B /* CounterState.swift in Sources */, + 87BA5F532706231100984D7B /* SceneDelegate.swift in Sources */, + 873FE73727062BB80054CF07 /* RootReducer.swift in Sources */, + 873FE767270633310054CF07 /* LazyNavigationEnvironment.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 87BA5F562706231100984D7B /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 87BA5F572706231100984D7B /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 87BA5F5B2706231200984D7B /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 87BA5F5C2706231200984D7B /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 872458742706058800A88871 /* 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++17"; + 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; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 872458752706058800A88871 /* 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++17"; + 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; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 87BA5F47270622E500984D7B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SwiftUICaseStudies/Preview Content\""; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstackswift.SwiftUICaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 87BA5F48270622E500984D7B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"SwiftUICaseStudies/Preview Content\""; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstackswift.SwiftUICaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 87BA5F602706231200984D7B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UIKitCaseStudies/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstackswift.UIKitCaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 87BA5F612706231200984D7B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = UIKitCaseStudies/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstackswift.UIKitCaseStudies; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8724585C2706058700A88871 /* Build configuration list for PBXProject "CaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 872458742706058800A88871 /* Debug */, + 872458752706058800A88871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 87BA5F49270622E500984D7B /* Build configuration list for PBXNativeTarget "SwiftUICaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87BA5F47270622E500984D7B /* Debug */, + 87BA5F48270622E500984D7B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 87BA5F5F2706231200984D7B /* Build configuration list for PBXNativeTarget "UIKitCaseStudies" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87BA5F602706231200984D7B /* Debug */, + 87BA5F612706231200984D7B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 87030D6F2710EED7009A5353 /* XCRemoteSwiftPackageReference "ReactiveCocoa" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/ReactiveCocoa/ReactiveCocoa.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 11.2.2; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 87030D722710EF07009A5353 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; + 87030D762710EFF5009A5353 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; + 878AF92C2710F0D30066E71C /* ReactiveCocoa */ = { + isa = XCSwiftPackageProductDependency; + package = 87030D6F2710EED7009A5353 /* XCRemoteSwiftPackageReference "ReactiveCocoa" */; + productName = ReactiveCocoa; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 872458592706058700A88871 /* 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 100644 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 100644 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 100644 index 0000000..4159c51 --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(SwiftUI).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 100644 index 0000000..770d9fa --- /dev/null +++ b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(UIKit).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..186b90e3fe4c070ff0bbef31ad85baa47a055d38 GIT binary patch literal 8152 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 YhpMf=!oN%>5_=LoEhEiJb^D0_0Whjby#N3J 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 100644 index 0000000..e4b96da --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,99 @@ +{ + "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" + }, + { + "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" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift b/Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift new file mode 100644 index 0000000..35abb2c --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift @@ -0,0 +1,14 @@ +import SwiftUI + +struct ContentView: View { + var body: some View { + Text("Hello, world!") + .padding() + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Preview Content/Preview Assets.xcassets/Contents.json b/Examples/CaseStudies/SwiftUICaseStudies/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift b/Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift new file mode 100644 index 0000000..1cfeafc --- /dev/null +++ b/Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct SwiftUICaseStudiesApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AppDelegate.swift b/Examples/CaseStudies/UIKitCaseStudies/AppDelegate.swift new file mode 100644 index 0000000..26cbdef --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/AppDelegate.swift @@ -0,0 +1,7 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + +} + diff --git a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..186b90e3fe4c070ff0bbef31ad85baa47a055d38 GIT binary patch literal 8152 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 YhpMf=!oN%>5_=LoEhEiJb^D0_0Whjby#N3J literal 0 HcmV?d00001 diff --git a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..e4b96da --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,99 @@ +{ + "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" + }, + { + "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" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift new file mode 100644 index 0000000..15ae7d4 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift @@ -0,0 +1,11 @@ +import ComposableArchitecture +import Foundation + +enum AuthAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case login + case changeRootScreen(RootScreen) +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthEnvironment.swift new file mode 100644 index 0000000..a96bc7e --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct AuthEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift new file mode 100644 index 0000000..8106c4f --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift @@ -0,0 +1,22 @@ +import ComposableArchitecture +import Foundation + +let AuthReducer = Reducer.combine( + + Reducer { state, action, environment in + switch action { + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + case .login: + return Effect(value: AuthAction.changeRootScreen(.main)) + default: + break + } + return .none + } +) +.debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthState.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthState.swift new file mode 100644 index 0000000..0f0ea3d --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthState.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct AuthState: Equatable { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift new file mode 100644 index 0000000..d4a2a00 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift @@ -0,0 +1,117 @@ +import ComposableArchitecture +import SwiftUI +import UIKit + + +final class AuthViewController: UIViewController { + + private var store: Store! + + private var viewStore: ViewStore! + + @IBOutlet weak private var btnLogin: UIButton! + + init(store: Store? = nil) { + super.init(nibName: nil, bundle: nil) + setStore(store: store) + } + + static func fromStoryboard(store: Store? = nil) -> AuthViewController { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "AuthViewController") as! AuthViewController + vc.setStore(store: store) + return vc + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func setStore(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: AuthState(), reducer: AuthReducer, environment: AuthEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: AuthAction.init)) + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + viewStore.action <~ btnLogin.reactive.controlEvents(.touchUpInside).map { _ in ViewAction.clickBtnLogin } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } + +} + +struct AuthViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = AuthViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + + init(state: AuthState) { + + } +} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case clickBtnLogin + + init(action: AuthAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension AuthState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension AuthAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + case .clickBtnLogin: + self = .login + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard b/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard b/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard new file mode 100644 index 0000000..d267725 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift new file mode 100644 index 0000000..2c85ba0 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift @@ -0,0 +1,11 @@ +import ComposableArchitecture +import Foundation + +enum CounterAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case decrementButtonTapped + case incrementButtonTapped +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterEnvironment.swift new file mode 100644 index 0000000..e756835 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct CounterEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift new file mode 100644 index 0000000..23841d5 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift @@ -0,0 +1,24 @@ +import ComposableArchitecture +import Foundation + +let CounterReducer = Reducer.combine( + + Reducer { state, action, environment in + switch action { + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + case .decrementButtonTapped: + state.count -= 1 + case .incrementButtonTapped: + state.count += 1 + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterState.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterState.swift new file mode 100644 index 0000000..a12eea9 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterState.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct CounterState: Equatable { + var count = 0 +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterViewController.swift new file mode 100644 index 0000000..d00298a --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterViewController.swift @@ -0,0 +1,129 @@ +import ComposableArchitecture +import SwiftUI +import UIKit +import ReactiveCocoa + +final class CounterViewController: UIViewController { + + private let store: Store + + private let viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: CounterState(), reducer: CounterReducer, environment: CounterEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: CounterAction.init)) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + view.backgroundColor = .systemBackground + let decrementButton = UIButton(type: .system) + decrementButton.setTitle("−", for: .normal) + let countLabel = UILabel() + countLabel.font = .monospacedDigitSystemFont(ofSize: 17, weight: .regular) + let incrementButton = UIButton(type: .system) + incrementButton.setTitle("+", for: .normal) + let rootStackView = UIStackView(arrangedSubviews: [ + decrementButton, + countLabel, + incrementButton, + ]) + rootStackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(rootStackView) + NSLayoutConstraint.activate([ + rootStackView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor), + rootStackView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor), + ]) + //binding view to store + viewStore.action <~ incrementButton.reactive.controlEvents(.touchUpInside).map {_ in ViewAction.incrementButtonTapped} + viewStore.action <~ decrementButton.reactive.controlEvents(.touchUpInside).map {_ in ViewAction.decrementButtonTapped} + //bind store to view + countLabel.reactive.text <~ viewStore.publisher.count.map {String($0)} + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } + +} + +struct CounterViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = CounterViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + var count = 0 + init(state: CounterState) { + count = state.count + } +} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case decrementButtonTapped + case incrementButtonTapped + + init(action: CounterAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension CounterState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension CounterAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + case .decrementButtonTapped: + self = .decrementButtonTapped + case .incrementButtonTapped: + self = .incrementButtonTapped + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableAction.swift b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableAction.swift new file mode 100644 index 0000000..402ae30 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableAction.swift @@ -0,0 +1,10 @@ +import ComposableArchitecture +import Foundation + +enum CountersTableAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case counter(index: Int, action: CounterAction) +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableEnvironment.swift new file mode 100644 index 0000000..6d14050 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct CountersTableEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift new file mode 100644 index 0000000..b7a5429 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift @@ -0,0 +1,22 @@ +import ComposableArchitecture +import Foundation + +let CountersTableReducer = Reducer.combine( + CounterReducer.forEach(state: \.counters, action: /CountersTableAction.counter(index:action:), environment: { _ in + .init() + }), + Reducer { state, action, enviroment in + switch action { + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + default: + break + } + return .none + } +) +.debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableState.swift b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableState.swift new file mode 100644 index 0000000..5fef0b9 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableState.swift @@ -0,0 +1,11 @@ +import ComposableArchitecture +import Foundation + +struct CountersTableState: Equatable { + var counters: [CounterState] = [CounterState(), + CounterState(), + CounterState(), + CounterState(), + CounterState()] + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableViewController.swift new file mode 100644 index 0000000..6117091 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableViewController.swift @@ -0,0 +1,133 @@ +import ComposableArchitecture +import SwiftUI +import UIKit + + +final class CountersTableViewController: UITableViewController { + + private let store: Store + + private let viewStore: ViewStore + + private let cellIdentifier = "Cell" + + private var dataSource: [CounterState] = [] + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: CountersTableState(), reducer: CountersTableReducer, environment: CountersTableEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: CountersTableAction.init)) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + self.title = "Lists" + self.tableView.register(UITableViewCell.self, forCellReuseIdentifier: cellIdentifier) + self.viewStore.publisher.counters.startWithValues { [weak self] in + guard let self = self else {return} + self.dataSource = $0 + self.tableView.reloadData() + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + self.dataSource.count + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: cellIdentifier, for: indexPath) + cell.accessoryType = .disclosureIndicator + cell.textLabel?.text = "\(self.dataSource[indexPath.row].count)" + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + navigationController?.pushViewController( + CounterViewController( + store: store.scope( + state: { $0.counters[indexPath.row] }, + action: { .counter(index: indexPath.row, action: $0) } + ) + ), + animated: true + ) + } +} + +struct CountersTableViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = CountersTableViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + var counters: [CounterState] = [] + init(state: CountersTableState) { + self.counters = state.counters + } +} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + + init(action: CountersTableAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension CountersTableState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension CountersTableAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationAction.swift b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationAction.swift new file mode 100644 index 0000000..df66182 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationAction.swift @@ -0,0 +1,12 @@ +import ComposableArchitecture +import Foundation + +enum EagerNavigationAction: Equatable { + case optionalCounter(CounterAction) + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case setNavigation(isActive: Bool) + case setNavigationIsActiveDelayCompleted +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationEnvironment.swift new file mode 100644 index 0000000..53bbd5c --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct EagerNavigationEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift new file mode 100644 index 0000000..87f544c --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift @@ -0,0 +1,38 @@ +import ComposableArchitecture +import Foundation + +let EagerNavigationReducer = Reducer.combine( + CounterReducer + .optional() + .pullback(state: \.optionalCounter, action: /EagerNavigationAction.optionalCounter, environment: { _ in + .init() + }), + Reducer { state, action, environment in + struct CancelId: Hashable {} + switch action { + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + case .setNavigation(isActive: true): + state.isNavigationActive = true + return Effect(value: .setNavigationIsActiveDelayCompleted) + .delay(1, on: QueueScheduler.main) + case .setNavigation(isActive: false): + state.isNavigationActive = false + state.optionalCounter = nil + return .none + case .setNavigationIsActiveDelayCompleted: + state.optionalCounter = CounterState() + return .none + case .optionalCounter: + return .none + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationState.swift b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationState.swift new file mode 100644 index 0000000..7b1148e --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationState.swift @@ -0,0 +1,7 @@ +import ComposableArchitecture +import Foundation + +struct EagerNavigationState: Equatable { + var isNavigationActive = false + var optionalCounter: CounterState? +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationViewController.swift new file mode 100644 index 0000000..25461b2 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationViewController.swift @@ -0,0 +1,144 @@ +import ComposableArchitecture +import SwiftUI +import UIKit + + +final class EagerNavigationViewController: UIViewController { + + private let store: Store + + private let viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: EagerNavigationState(), reducer: EagerNavigationReducer, environment: EagerNavigationEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: EagerNavigationAction.init)) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + title = "Navigate and load" + view.backgroundColor = .systemBackground + let button = UIButton(type: .system) + button.setTitle("Load optional counter", for: .normal) + button.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(button) + NSLayoutConstraint.activate([ + button.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor), + button.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor), + ]) + viewStore.action <~ button.reactive.controlEvents(.touchUpInside).map {_ in ViewAction.setNavigation(isActive: true)} + viewStore.publisher.isNavigationActive.startWithValues { [weak self] isNavigationActive in + guard let self = self else { return } + if isNavigationActive { + self.navigationController?.pushViewController( + IfLetStoreController( + store: self.store + .scope(state: \.optionalCounter, action: EagerNavigationAction.optionalCounter), + then: CounterViewController.init(store:), + else: ActivityIndicatorViewController.init + ), + animated: true + ) + } else { + self.navigationController?.popToViewController(self, animated: true) + } + } + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if !self.isMovingToParent { + self.viewStore.send(.setNavigation(isActive: false)) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } +} + +struct EagerNavigationViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = EagerNavigationViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + var isNavigationActive = false + var optionalCounter: CounterState? + init(state: EagerNavigationState) { + self.isNavigationActive = state.isNavigationActive + self.optionalCounter = state.optionalCounter + } +} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case optionalCounter(CounterAction) + case setNavigation(isActive: Bool) + case setNavigationIsActiveDelayCompleted + + init(action: EagerNavigationAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension EagerNavigationState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension EagerNavigationAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + case .optionalCounter(let counterAction): + self = .optionalCounter(counterAction) + case .setNavigation(let isActive): + self = .setNavigation(isActive: isActive) + case .setNavigationIsActiveDelayCompleted: + self = .setNavigationIsActiveDelayCompleted + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/Info.plist b/Examples/CaseStudies/UIKitCaseStudies/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift new file mode 100644 index 0000000..940151f --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift @@ -0,0 +1,20 @@ +import UIKit + +final class ActivityIndicatorViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .white + let activityIndicator = UIActivityIndicatorView() + activityIndicator.startAnimating() + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(activityIndicator) + + NSLayoutConstraint.activate([ + activityIndicator.centerXAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.centerXAnchor), + activityIndicator.centerYAnchor.constraint( + equalTo: view.safeAreaLayoutGuide.centerYAnchor), + ]) + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift b/Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift new file mode 100644 index 0000000..9fe1209 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift @@ -0,0 +1,50 @@ +import ComposableArchitecture +import UIKit +import ReactiveSwift +import ReactiveCocoa + +final class IfLetStoreController: UIViewController { + let store: Store + let ifDestination: (Store) -> UIViewController + let elseDestination: () -> UIViewController + + private var viewController = UIViewController() { + willSet { + self.viewController.willMove(toParent: nil) + self.viewController.view.removeFromSuperview() + self.viewController.removeFromParent() + self.addChild(newValue) + self.view.addSubview(newValue.view) + newValue.didMove(toParent: self) + } + } + + init(store: Store, + then ifDestination: @escaping (Store) -> UIViewController, + else elseDestination: @escaping () -> UIViewController + ) { + self.store = store + self.ifDestination = ifDestination + self.elseDestination = elseDestination + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + + self.store.ifLet( + then: { [weak self] store in + guard let self = self else { return } + self.viewController = self.ifDestination(store) + }, + else: { [weak self] in + guard let self = self else { return } + self.viewController = self.elseDestination() + } + ) + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationAction.swift b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationAction.swift new file mode 100644 index 0000000..283186a --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationAction.swift @@ -0,0 +1,12 @@ +import ComposableArchitecture +import Foundation + +enum LazyNavigationAction: Equatable { + case optionalCounter(CounterAction) + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case setNavigation(isActive: Bool) + case setNavigationIsActiveDelayCompleted +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationEnvironment.swift new file mode 100644 index 0000000..69c27f4 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct LazyNavigationEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift new file mode 100644 index 0000000..6d84ee6 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift @@ -0,0 +1,38 @@ +import ComposableArchitecture +import Foundation + +let LazyNavigationReducer = Reducer.combine( + CounterReducer + .optional() + .pullback(state: \.optionalCounter,action: /LazyNavigationAction.optionalCounter,environment: { _ in + .init() + }), + Reducer { state, action, environment in + struct CancelId: Hashable {} + switch action { + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + case .setNavigation(isActive: true): + state.isActivityIndicatorHidden = false + return Effect(value: .setNavigationIsActiveDelayCompleted) + .delay(1, on: QueueScheduler.main) + case .setNavigation(isActive: false): + state.optionalCounter = nil + return .none + case .setNavigationIsActiveDelayCompleted: + state.isActivityIndicatorHidden = true + state.optionalCounter = CounterState() + return .none + case .optionalCounter: + return .none + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationState.swift b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationState.swift new file mode 100644 index 0000000..b93e820 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationState.swift @@ -0,0 +1,7 @@ +import ComposableArchitecture +import Foundation + +struct LazyNavigationState: Equatable { + var optionalCounter: CounterState? + var isActivityIndicatorHidden = true +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationViewController.swift new file mode 100644 index 0000000..ef23bb5 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationViewController.swift @@ -0,0 +1,146 @@ +import ComposableArchitecture +import SwiftUI +import UIKit + + +final class LazyNavigationViewController: UIViewController { + + private let store: Store + + private let viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: LazyNavigationState(), reducer: LazyNavigationReducer, environment: LazyNavigationEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: LazyNavigationAction.init)) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + title = "Load then navigate" + view.backgroundColor = .systemBackground + let button = UIButton(type: .system) + button.setTitle("Load optional counter", for: .normal) + let activityIndicator = UIActivityIndicatorView() + activityIndicator.startAnimating() + let rootStackView = UIStackView(arrangedSubviews: [ + button, + activityIndicator, + ]) + rootStackView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(rootStackView) + NSLayoutConstraint.activate([ + rootStackView.centerXAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerXAnchor), + rootStackView.centerYAnchor.constraint(equalTo: self.view.safeAreaLayoutGuide.centerYAnchor), + ]) + viewStore.action <~ button.reactive.controlEvents(.touchUpInside).map{_ in ViewAction.setNavigation(isActive: true)} + viewStore.publisher.isActivityIndicatorHidden + .assign(to: \.isHidden, on: activityIndicator) + store.scope(state: \.optionalCounter, action: LazyNavigationAction.optionalCounter) + .ifLet( + then: { [weak self] store in + self?.navigationController?.pushViewController( + CounterViewController(store: store), animated: true) + }, + else: { [weak self] in + guard let self = self else { return } + self.navigationController?.popToViewController(self, animated: true) + } + ) + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + if !self.isMovingToParent { + self.viewStore.send(.setNavigation(isActive: false)) + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } +} + +struct LazyNavigationViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = LazyNavigationViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + var optionalCounter: CounterState? + var isActivityIndicatorHidden = true + init(state: LazyNavigationState) { + self.optionalCounter = state.optionalCounter + self.isActivityIndicatorHidden = state.isActivityIndicatorHidden + }} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case optionalCounter(CounterAction) + case setNavigation(isActive: Bool) + case setNavigationIsActiveDelayCompleted + + init(action: LazyNavigationAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension LazyNavigationState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension LazyNavigationAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + case .optionalCounter(let counterAction): + self = .optionalCounter(counterAction) + case .setNavigation(let isActive): + self = .setNavigation(isActive: isActive) + case .setNavigationIsActiveDelayCompleted: + self = .setNavigationIsActiveDelayCompleted + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainAction.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainAction.swift new file mode 100644 index 0000000..1d33db4 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainAction.swift @@ -0,0 +1,11 @@ +import ComposableArchitecture +import Foundation + +enum MainAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case logout + case changRootScreen(RootScreen) +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainEnvironment.swift new file mode 100644 index 0000000..19d428c --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct MainEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift new file mode 100644 index 0000000..78a3864 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift @@ -0,0 +1,22 @@ +import ComposableArchitecture +import Foundation + +let MainReducer = Reducer.combine( + + Reducer { state, action, environment in + switch action { + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + case .logout: + return Effect(value: MainAction.changRootScreen(.auth)) + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainState.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainState.swift new file mode 100644 index 0000000..11846cf --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainState.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct MainState: Equatable { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift new file mode 100644 index 0000000..47889ad --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift @@ -0,0 +1,166 @@ +import ComposableArchitecture +import SwiftUI +import UIKit + + +struct CaseStudy { + let title: String + let viewController: () -> UIViewController + + init(title: String, viewController: @autoclosure @escaping () -> UIViewController) { + self.title = title + self.viewController = viewController + } +} + +let dataSource: [CaseStudy] = [ + CaseStudy(title: "Basics",viewController: CounterViewController()), + CaseStudy(title: "Lists",viewController: CountersTableViewController()), + CaseStudy(title: "Eager Navigation",viewController: EagerNavigationViewController()), + CaseStudy(title: "Lazy Navigation",viewController: LazyNavigationViewController()), +] + +final class MainViewController: UIViewController { + + private var store: Store! + + private var viewStore: ViewStore! + + @IBOutlet weak private var tableView: UITableView! + + init(store: Store? = nil) { + super.init(nibName: nil, bundle: nil) + setStore(store: store) + } + + static func fromStoryboard(store: Store? = nil) -> MainViewController { + let storyboard = UIStoryboard(name: "Main", bundle: nil) + let vc = storyboard.instantiateViewController(withIdentifier: "MainViewController") as! MainViewController + vc.setStore(store: store) + return vc + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + } + + private func setStore(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: MainState(), reducer: MainReducer, environment: MainEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: MainAction.init)) + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + + //navigation + title = "Case Studies" + navigationController?.navigationBar.prefersLargeTitles = true + let buttonLogout = UIButton(type: .system) + buttonLogout.setTitle("Logout", for: .normal) + viewStore.action <~ buttonLogout.reactive.controlEvents(.touchUpInside).map{_ in ViewAction.logout} + let rightBarButtonItem = UIBarButtonItem(customView: buttonLogout) + navigationItem.rightBarButtonItem = rightBarButtonItem + tableView.delegate = self + tableView.dataSource = self + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } +} + +extension MainViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + dataSource.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath)-> UITableViewCell { + let caseStudy = dataSource[indexPath.row] + let cell = UITableViewCell() + cell.accessoryType = .disclosureIndicator + cell.textLabel?.text = caseStudy.title + return cell + } +} + +extension MainViewController: UITableViewDelegate { + + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + let caseStudy = dataSource[indexPath.row] + self.navigationController?.pushViewController(caseStudy.viewController(), animated: true) + } +} + +struct MainViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = MainViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + + init(state: MainState) { + + } +} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + case logout + + init(action: MainAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension MainState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension MainAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + case .logout: + self = .logout + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootAction.swift b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootAction.swift new file mode 100644 index 0000000..675506b --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootAction.swift @@ -0,0 +1,11 @@ +import ComposableArchitecture +import Foundation + +enum RootAction: Equatable { + case authAction(AuthAction) + case mainAction(MainAction) + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootEnvironment.swift b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootEnvironment.swift new file mode 100644 index 0000000..6b791ec --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct RootEnvironment { + +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift new file mode 100644 index 0000000..be85597 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift @@ -0,0 +1,29 @@ +import ComposableArchitecture +import Foundation + +let RootReducer = Reducer.combine( + AuthReducer.pullback(state: \.authState, action: /RootAction.authAction, environment: { _ in + .init() + }), + MainReducer.pullback(state: \.mainState, action: /RootAction.mainAction, environment: { _ in + .init() + }), + Reducer { state, action, environment in + switch action { + case .authAction(.changeRootScreen(let screen)): + state.rootScreen = screen + case .mainAction(.changRootScreen(let screen)): + state.rootScreen = screen + case .viewDidLoad: + break + case .viewWillAppear: + break + case .viewWillDisappear: + break + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootState.swift b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootState.swift new file mode 100644 index 0000000..b0b3276 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootState.swift @@ -0,0 +1,13 @@ +import ComposableArchitecture +import Foundation + +struct RootState: Equatable { + var authState = AuthState() + var mainState = MainState() + var rootScreen: RootScreen = .main +} + +enum RootScreen: Equatable { + case main + case auth +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootViewController.swift new file mode 100644 index 0000000..dc7e0a0 --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootViewController.swift @@ -0,0 +1,126 @@ +import ComposableArchitecture +import SwiftUI +import UIKit + + +final class RootViewController: UIViewController { + + private let store: Store + + private let viewStore: ViewStore + + private var viewController = UIViewController() { + willSet { + self.viewController.willMove(toParent: nil) + self.viewController.view.removeFromSuperview() + self.viewController.removeFromParent() + self.addChild(newValue) + newValue.view.frame = self.view.frame + self.view.addSubview(newValue.view) + newValue.didMove(toParent: self) + } + } + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: RootState(), reducer: RootReducer, environment: RootEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: RootAction.init)) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + viewStore.send(.viewDidLoad) + view.backgroundColor = .white + viewStore.publisher.rootScreen.startWithValues { [weak self] screen in + guard let self = self else {return} + switch screen { + case .main: + let vc = MainViewController.fromStoryboard(store: self.store.scope(state: \.mainState, action: RootAction.mainAction)) + let nav = UINavigationController(rootViewController: vc) + self.viewController = nav + case .auth: + let vc = AuthViewController.fromStoryboard(store: self.store.scope(state: \.authState, action: RootAction.authAction)) + self.viewController = vc + } + } + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + viewStore.send(.viewWillAppear) + } + + override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated) + viewStore.send(.viewWillDisappear) + } + +} + +struct RootViewController_Previews: PreviewProvider { + static var previews: some View { + let vc = RootViewController() + UIViewRepresented(makeUIView: { _ in vc.view }) + } +} + +fileprivate struct ViewState: Equatable { + var rootScreen: RootScreen = .main + init(state: RootState) { + rootScreen = state.rootScreen + } +} + +fileprivate enum ViewAction: Equatable { + case viewDidLoad + case viewWillAppear + case viewWillDisappear + case none + + init(action: RootAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} + +fileprivate extension RootState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } + +} + +fileprivate extension RootAction { + + init(action: ViewAction) { + switch action { + case .viewDidLoad: + self = .viewDidLoad + case .viewWillAppear: + self = .viewWillAppear + case .viewWillDisappear: + self = .viewWillDisappear + default: + self = .none + } + } +} diff --git a/Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift b/Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift new file mode 100644 index 0000000..0fa5fab --- /dev/null +++ b/Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift @@ -0,0 +1,13 @@ +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard let _ = (scene as? UIWindowScene) else { return } + self.window = (scene as? UIWindowScene).map(UIWindow.init(windowScene:)) + self.window?.rootViewController = UINavigationController( + rootViewController: RootViewController()) + self.window?.makeKeyAndVisible() + } +} + diff --git a/Examples/Package.swift b/Examples/Package.swift new file mode 100644 index 0000000..cd451c3 --- /dev/null +++ b/Examples/Package.swift @@ -0,0 +1,9 @@ +// swift-tools-version:5.2 + +import PackageDescription + +let package = Package( + name: "Examples", + products: [], + targets: [] +) diff --git a/Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Todos/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Todos/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..c136eaf --- /dev/null +++ b/Examples/Todos/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,148 @@ +{ + "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" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "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" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Todos/Shared/Assets.xcassets/Contents.json b/Examples/Todos/Shared/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Examples/Todos/Shared/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift new file mode 100644 index 0000000..1c13ae7 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift @@ -0,0 +1,10 @@ +import ComposableArchitecture +import Foundation + +enum AuthAction: Equatable { + case viewOnAppear + case viewOnDisappear + case none + case login + case changeRootScreen(RootScreen) +} diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthEnvironment.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthEnvironment.swift new file mode 100644 index 0000000..433425c --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct AuthEnvironment { + +} diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift new file mode 100644 index 0000000..8a7a1c2 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift @@ -0,0 +1,20 @@ +import ComposableArchitecture +import Foundation + +let AuthReducer = Reducer.combine( + + Reducer { state, action, environment in + switch action { + case .viewOnAppear: + break + case .viewOnDisappear: + break + case .login: + return Effect(value: AuthAction.changeRootScreen(.main)) + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthState.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthState.swift new file mode 100644 index 0000000..fe78e09 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthState.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct AuthState: Equatable { + +} diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthView.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthView.swift new file mode 100644 index 0000000..356d855 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthView.swift @@ -0,0 +1,89 @@ +import ComposableArchitecture +import SwiftUI + +struct AuthView: View { + + private let store: Store + + @ObservedObject + private var viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: AuthState(), reducer: AuthReducer, environment: AuthEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: AuthAction.init)) + } + + var body: some View { + ZStack { + Button("Login") { + viewStore.send(.login) + } + } + .onAppear { + viewStore.send(.viewOnAppear) + } + .onDisappear { + viewStore.send(.viewOnDisappear) + } + } +} + +struct AuthView_Previews: PreviewProvider { + static var previews: some View { + AuthView() + } +} + +fileprivate struct ViewState: Equatable { + + init(state: AuthState) { + + } +} + +fileprivate enum ViewAction: Equatable { + case viewOnAppear + case viewOnDisappear + case none + case login + init(action: AuthAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + default: + self = .none + } + } +} + +fileprivate extension AuthState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } + +} + +fileprivate extension AuthAction { + + init(action: ViewAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + case .login: + self = .login + default: + self = .none + } + } +} diff --git a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterAction.swift b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterAction.swift new file mode 100644 index 0000000..333a112 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterAction.swift @@ -0,0 +1,10 @@ +import ComposableArchitecture +import Foundation + +enum CounterAction: Equatable { + case viewOnAppear + case viewOnDisappear + case none + case increment + case decrement +} diff --git a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterEnvironment.swift b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterEnvironment.swift new file mode 100644 index 0000000..824cb72 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct CounterEnvironment { + +} diff --git a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift new file mode 100644 index 0000000..11d83fe --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift @@ -0,0 +1,21 @@ +import ComposableArchitecture +import Foundation + +let CounterReducer = Reducer.combine( + Reducer { state, action, environment in + switch action { + case .viewOnAppear: + break + case .viewOnDisappear: + break + case .decrement: + state.count -= 1 + case .increment: + state.count += 1 + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterState.swift b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterState.swift new file mode 100644 index 0000000..3010941 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterState.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct CounterState: Equatable { + var count: Int = 0 +} diff --git a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterView.swift b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterView.swift new file mode 100644 index 0000000..9021b3b --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterView.swift @@ -0,0 +1,99 @@ +import ComposableArchitecture +import SwiftUI + +struct CounterView: View { + + private let store: Store + + @ObservedObject + private var viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: CounterState(), reducer: CounterReducer, environment: CounterEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: CounterAction.init)) + } + + var body: some View { + ZStack { + HStack { + Button { + viewStore.send(.increment) + } label: { + Text("+") + } + Text("\(viewStore.count)") + Button { + viewStore.send(.decrement) + } label: { + Text("-") + } + } + } + .onAppear { + viewStore.send(.viewOnAppear) + } + .onDisappear { + viewStore.send(.viewOnDisappear) + } + } +} + +struct CounterView_Previews: PreviewProvider { + static var previews: some View { + CounterView() + } +} + +fileprivate struct ViewState: Equatable { + var count: String = "" + init(state: CounterState) { + count = state.count.toString() + } +} + +fileprivate enum ViewAction: Equatable { + case viewOnAppear + case viewOnDisappear + case none + case increment + case decrement + init(action: CounterAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + default: + self = .none + } + } +} + +fileprivate extension CounterState { + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } +} + +fileprivate extension CounterAction { + init(action: ViewAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + case .decrement: + self = .decrement + case .increment: + self = .increment + default: + self = .none + } + } +} diff --git a/Examples/Todos/Shared/TodoApp/MainScreen/MainAction.swift b/Examples/Todos/Shared/TodoApp/MainScreen/MainAction.swift new file mode 100644 index 0000000..1042dfc --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/MainScreen/MainAction.swift @@ -0,0 +1,21 @@ +import ComposableArchitecture +import Foundation + +enum MainAction: BindableAction, Equatable { + case counterAction(CounterAction) + case viewOnAppear + case viewOnDisappear + case none + case binding(_ action: BindingAction) + case getTodo + case toggleTodo(Todo) + case responseTodo(Data) + case createTodo + case responseCreateTodo(Data) + case updateTodo(Todo) + case responseUpdateTodo(Data) + case deleteTodo(Todo) + case reponseDeleteTodo(Data) + case logout + case changeRootScreen(RootScreen) +} diff --git a/Examples/Todos/Shared/TodoApp/MainScreen/MainEnvironment.swift b/Examples/Todos/Shared/TodoApp/MainScreen/MainEnvironment.swift new file mode 100644 index 0000000..19d428c --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/MainScreen/MainEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct MainEnvironment { + +} diff --git a/Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift b/Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift new file mode 100644 index 0000000..f8f395f --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift @@ -0,0 +1,99 @@ +import ComposableArchitecture +import Foundation +import AnyRequest +import ConvertSwift + +let MainReducer = Reducer.combine( + CounterReducer.pullback(state: \.counterState, action: /MainAction.counterAction, environment: { _ in + .init() + }), + Reducer { state, action, environment in + let urlString = "http://127.0.0.1:8080/todos" + switch action { + /// update state + case .binding(let bindingAction): + break + case .toggleTodo(let todo): + if var todo = state.todos.filter({$0 == todo}).first { + todo.isCompleted.toggle() + return Effect(value: MainAction.updateTodo(todo)) + } + /// networking + case .getTodo: + state.isLoading = true + state.todos.removeAll() + let request = Request { + RMethod(.get) + RUrl(urlString: urlString) + } + return request + .delay(for: .seconds(1), scheduler: UIScheduler.shared) // fake loading + .compactMap {$0.data} + .map(MainAction.responseTodo).eraseToEffect() + case .responseTodo(let json): + state.isLoading = false + if let todos = json.toModel([Todo].self) { + for todo in todos { + state.todos.append(todo) + } + } + case .createTodo: + if state.title.isEmpty { + return .none + } + var title = state.title + state.title = "" + let todo = Todo(id: nil, title: title, isCompleted: false) + let request = Request { + RUrl(urlString: urlString) + REncoding(.json) + RMethod(.post) + Rbody(todo.toData()) + } + return request + .compactMap {$0.data} + .map(MainAction.responseCreateTodo).eraseToEffect() + case .responseCreateTodo(let data): + if let todo = data.toModel(Todo.self) { + state.todos.append(todo) + } + case .updateTodo(let todo): + let request = Request { + REncoding(.json) + RUrl(urlString: urlString).withPath(todo.id) + RMethod(.post) + Rbody(todo.toData()) + } + return request + .compactMap {$0.data} + .map(MainAction.responseUpdateTodo).eraseToEffect() + case .responseUpdateTodo(let data): + if let todo = data.toModel(Todo.self) { + state.todos.updateOrAppend(todo) + } + case .deleteTodo(let todo): + let request = Request { + RUrl(urlString: urlString).withPath(todo.id) + RMethod(.delete) + } + return request + .compactMap {$0.data} + .map(MainAction.reponseDeleteTodo).eraseToEffect() + case .reponseDeleteTodo(let data): + if let todo = data.toModel(Todo.self) { + state.todos.remove(todo) + } + case .viewOnAppear: + return Effect(value: MainAction.getTodo) + case .viewOnDisappear: + state = MainState() + case .logout: + return Effect(value: MainAction.changeRootScreen(.auth)) + default: + break + } + return .none + } +) + .binding() + .debug() diff --git a/Examples/Todos/Shared/TodoApp/MainScreen/MainState.swift b/Examples/Todos/Shared/TodoApp/MainScreen/MainState.swift new file mode 100644 index 0000000..07282d0 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/MainScreen/MainState.swift @@ -0,0 +1,9 @@ +import ComposableArchitecture +import Foundation + +struct MainState: Equatable { + var counterState = CounterState() + @BindableState var title: String = "" + var todos: IdentifiedArrayOf = IdentifiedArray() + var isLoading: Bool = false +} diff --git a/Examples/Todos/Shared/TodoApp/MainScreen/MainView.swift b/Examples/Todos/Shared/TodoApp/MainScreen/MainView.swift new file mode 100644 index 0000000..b6cf5a3 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/MainScreen/MainView.swift @@ -0,0 +1,193 @@ +import ComposableArchitecture +import SwiftUI + +struct MainView: View { + + private let store: Store + + @ObservedObject + private var viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: MainState(), reducer: MainReducer, environment: MainEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: MainAction.init)) + } + + var body: some View { + ZStack { + List { + HStack { + Spacer() + Text(viewStore.isLoading ? "Loading" : "Reload") + .bold() + Spacer() + } + .onTapGesture { + viewStore.send(.getTodo) + } + HStack { + TextField("title", text: viewStore.binding(\.$title)) + Button(action: { + viewStore.send(.createTodo) + }, label: { + Text("Create") + .bold() + .foregroundColor(viewStore.title.isEmpty ? Color.gray : Color.green) + }) + .disabled(viewStore.title.isEmpty) + } + + ForEach(viewStore.todos) { todo in + HStack { + HStack { + Image(systemName: todo.isCompleted ? "checkmark.square" : "square") + Text(todo.title) + .underline(todo.isCompleted, color: Color.black) + Spacer() + } + .contentShape(Rectangle()) + .onTapGesture { + viewStore.send(.toggleTodo(todo)) + } + Button(action: { + viewStore.send(.deleteTodo(todo)) + }, label: { + Text("Delete") + .foregroundColor(Color.gray) + }) + } + } +#if os(iOS) + .listStyle(PlainListStyle()) +#else + .listStyle(PlainListStyle()) +#endif + .padding(.all, 0) + } + .padding(.all, 0) +#if os(macOS) + .toolbar { + ToolbarItem(placement: .status) { + HStack { + CounterView(store: store.scope(state: \.counterState, action: MainAction.counterAction)) + Spacer() + Button(action: { + viewStore.send(.logout) + }, label: { + Text("Logout") + .foregroundColor(Color.blue) + }) + } + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) +#endif +#if os(iOS) + .navigationTitle("Todos") + .navigationViewStyle(.stack) + .navigationBarItems(leading: leadingBarItems, trailing: trailingBarItems) + .embedNavigationView() +#endif + } + .onAppear { + viewStore.send(.viewOnAppear) + } + .onDisappear { + viewStore.send(.viewOnDisappear) + } + } +} + +extension MainView { + + private var leadingBarItems: some View { + CounterView(store: store.scope(state: \.counterState, action: MainAction.counterAction)) + } + + private var trailingBarItems: some View { + Button(action: { + viewStore.send(.logout) + }, label: { + Text("Logout") + .foregroundColor(Color.blue) + }) + } +} + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + MainView() + } +} + +fileprivate struct ViewState: Equatable { + @BindableState var title: String = "" + var todos: IdentifiedArrayOf = [] + var isLoading: Bool = false + init(state: MainState) { + self.title = state.title + self.todos = state.todos + self.isLoading = state.isLoading + } +} + +fileprivate enum ViewAction: BindableAction, Equatable { + case binding(_ action: BindingAction) + case getTodo + case toggleTodo(Todo) + case createTodo + case updateTodo(Todo) + case deleteTodo(Todo) + case logout + case viewOnAppear + case viewOnDisappear + case none + + init(action: MainAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + default: + self = .none + } + } +} + +fileprivate extension MainState { + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + self.title = newValue.title + } + } +} + +fileprivate extension MainAction { + init(action: ViewAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + case .binding(let bindingAction): + self = .binding(bindingAction.pullback(\.viewState)) + case .getTodo: + self = .getTodo + case .toggleTodo(let todo): + self = .toggleTodo(todo) + case .createTodo: + self = .createTodo + case .deleteTodo(let todo): + self = .deleteTodo(todo) + case .logout: + self = .logout + default: + self = .none + } + } +} diff --git a/Examples/Todos/Shared/TodoApp/Models/Todo.swift b/Examples/Todos/Shared/TodoApp/Models/Todo.swift new file mode 100644 index 0000000..db47fe3 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/Models/Todo.swift @@ -0,0 +1,7 @@ +import Foundation + +struct Todo: Codable, Identifiable, Equatable { + var id: String? + var title: String + var isCompleted: Bool +} diff --git a/Examples/Todos/Shared/TodoApp/RootScreen/RootAction.swift b/Examples/Todos/Shared/TodoApp/RootScreen/RootAction.swift new file mode 100644 index 0000000..e8f194e --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/RootScreen/RootAction.swift @@ -0,0 +1,10 @@ +import ComposableArchitecture +import Foundation + +enum RootAction: Equatable { + case authAction(AuthAction) + case mainAction(MainAction) + case viewOnAppear + case viewOnDisappear + case none +} diff --git a/Examples/Todos/Shared/TodoApp/RootScreen/RootEnvironment.swift b/Examples/Todos/Shared/TodoApp/RootScreen/RootEnvironment.swift new file mode 100644 index 0000000..6b791ec --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/RootScreen/RootEnvironment.swift @@ -0,0 +1,6 @@ +import ComposableArchitecture +import Foundation + +struct RootEnvironment { + +} diff --git a/Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift b/Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift new file mode 100644 index 0000000..ed56b31 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift @@ -0,0 +1,27 @@ +import ComposableArchitecture +import Foundation + +let RootReducer = Reducer.combine( + MainReducer.pullback(state: \.mainState, action: /RootAction.mainAction, environment: { _ in + .init() + }), + AuthReducer.pullback(state: \.authState, action: /RootAction.authAction, environment: { _ in + .init() + }), + Reducer { state, action, environment in + switch action { + case .authAction(.changeRootScreen(let screen)): + state.rootScreen = screen + case .mainAction(.changeRootScreen(let screen)): + state.rootScreen = screen + case .viewOnAppear: + break + case .viewOnDisappear: + break + default: + break + } + return .none + } +) + .debug() diff --git a/Examples/Todos/Shared/TodoApp/RootScreen/RootState.swift b/Examples/Todos/Shared/TodoApp/RootScreen/RootState.swift new file mode 100644 index 0000000..b0b3276 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/RootScreen/RootState.swift @@ -0,0 +1,13 @@ +import ComposableArchitecture +import Foundation + +struct RootState: Equatable { + var authState = AuthState() + var mainState = MainState() + var rootScreen: RootScreen = .main +} + +enum RootScreen: Equatable { + case main + case auth +} diff --git a/Examples/Todos/Shared/TodoApp/RootScreen/RootView.swift b/Examples/Todos/Shared/TodoApp/RootScreen/RootView.swift new file mode 100644 index 0000000..4b9acf3 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/RootScreen/RootView.swift @@ -0,0 +1,90 @@ +import ComposableArchitecture +import SwiftUI + +struct RootView: View { + + private let store: Store + + @ObservedObject + private var viewStore: ViewStore + + init(store: Store? = nil) { + let unwrapStore = store ?? Store(initialState: RootState(), reducer: RootReducer, environment: RootEnvironment()) + self.store = unwrapStore + self.viewStore = ViewStore(unwrapStore.scope(state: ViewState.init, action: RootAction.init)) + } + + var body: some View { + ZStack { + switch viewStore.rootScreen { + case .main: + MainView(store: store.scope(state: \.mainState, action: RootAction.mainAction)) + case .auth: + AuthView(store: store.scope(state: \.authState, action: RootAction.authAction)) + } + } + .onAppear { + viewStore.send(.viewOnAppear) + } + .onDisappear { + viewStore.send(.viewOnDisappear) + } + } +} + +struct RootView_Previews: PreviewProvider { + static var previews: some View { + RootView() + } +} + +fileprivate struct ViewState: Equatable { + var rootScreen: RootScreen = .auth + init(state: RootState) { + self.rootScreen = state.rootScreen + } +} + +fileprivate enum ViewAction: Equatable { + case viewOnAppear + case viewOnDisappear + case none + + init(action: RootAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + default: + self = .none + } + } +} + +fileprivate extension RootState { + + var viewState: ViewState { + get { + ViewState(state: self) + } + set { + + } + } + +} + +fileprivate extension RootAction { + + init(action: ViewAction) { + switch action { + case .viewOnAppear: + self = .viewOnAppear + case .viewOnDisappear: + self = .viewOnDisappear + default: + self = .none + } + } +} diff --git a/Examples/Todos/Shared/TodoApp/View+/View+.swift b/Examples/Todos/Shared/TodoApp/View+/View+.swift new file mode 100644 index 0000000..313f571 --- /dev/null +++ b/Examples/Todos/Shared/TodoApp/View+/View+.swift @@ -0,0 +1,9 @@ +import SwiftUI + +extension View { + func embedNavigationView() -> some View { + NavigationView { + self + } + } +} diff --git a/Examples/Todos/Shared/TodosApp.swift b/Examples/Todos/Shared/TodosApp.swift new file mode 100644 index 0000000..117c539 --- /dev/null +++ b/Examples/Todos/Shared/TodosApp.swift @@ -0,0 +1,13 @@ +import SwiftUI + +@main +struct TodosApp: App { + var body: some Scene { + WindowGroup { + RootView() +#if os(macOS) + .frame(minWidth: 700, idealWidth: 700, maxWidth: .infinity, minHeight: 500, idealHeight: 500, maxHeight: .infinity, alignment: .center) +#endif + } + } +} diff --git a/Examples/Todos/Todos.xcodeproj/project.pbxproj b/Examples/Todos/Todos.xcodeproj/project.pbxproj new file mode 100644 index 0000000..926855a --- /dev/null +++ b/Examples/Todos/Todos.xcodeproj/project.pbxproj @@ -0,0 +1,750 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 8724584A270603C800A88871 /* TodosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8724583A270603C700A88871 /* TodosApp.swift */; }; + 8724584B270603C800A88871 /* TodosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8724583A270603C700A88871 /* TodosApp.swift */; }; + 8724584E270603C800A88871 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8724583C270603C800A88871 /* Assets.xcassets */; }; + 8724584F270603C800A88871 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8724583C270603C800A88871 /* Assets.xcassets */; }; + 875BCFB32710EAD50015E662 /* AnyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 875BCFB22710EAD50015E662 /* AnyRequest */; }; + 875BCFB62710EAEF0015E662 /* ConvertSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 875BCFB52710EAEF0015E662 /* ConvertSwift */; }; + 875BCFB92710EB0C0015E662 /* Json in Frameworks */ = {isa = PBXBuildFile; productRef = 875BCFB82710EB0C0015E662 /* Json */; }; + 875BCFBB2710ED8E0015E662 /* AnyRequest in Frameworks */ = {isa = PBXBuildFile; productRef = 875BCFBA2710ED8E0015E662 /* AnyRequest */; }; + 875BCFBD2710ED8E0015E662 /* ConvertSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 875BCFBC2710ED8E0015E662 /* ConvertSwift */; }; + 875BCFBF2710ED8E0015E662 /* Json in Frameworks */ = {isa = PBXBuildFile; productRef = 875BCFBE2710ED8E0015E662 /* Json */; }; + 8769538A2706078100EB9008 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 876953892706078100EB9008 /* ComposableArchitecture */; }; + 8769538C2706078600EB9008 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 8769538B2706078600EB9008 /* ComposableArchitecture */; }; + 878AF95927110B020066E71C /* CounterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95427110B020066E71C /* CounterAction.swift */; }; + 878AF95A27110B020066E71C /* CounterAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95427110B020066E71C /* CounterAction.swift */; }; + 878AF95B27110B020066E71C /* CounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95527110B020066E71C /* CounterView.swift */; }; + 878AF95C27110B020066E71C /* CounterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95527110B020066E71C /* CounterView.swift */; }; + 878AF95D27110B020066E71C /* CounterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95627110B020066E71C /* CounterEnvironment.swift */; }; + 878AF95E27110B020066E71C /* CounterEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95627110B020066E71C /* CounterEnvironment.swift */; }; + 878AF95F27110B020066E71C /* CounterReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95727110B020066E71C /* CounterReducer.swift */; }; + 878AF96027110B020066E71C /* CounterReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95727110B020066E71C /* CounterReducer.swift */; }; + 878AF96127110B020066E71C /* CounterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95827110B020066E71C /* CounterState.swift */; }; + 878AF96227110B020066E71C /* CounterState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF95827110B020066E71C /* CounterState.swift */; }; + 878AF96527110B4C0066E71C /* View+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF96427110B4C0066E71C /* View+.swift */; }; + 878AF96627110B4C0066E71C /* View+.swift in Sources */ = {isa = PBXBuildFile; fileRef = 878AF96427110B4C0066E71C /* View+.swift */; }; + 87A8E14E27065E9500E2D797 /* MainReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14927065E9500E2D797 /* MainReducer.swift */; }; + 87A8E14F27065E9500E2D797 /* MainReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14927065E9500E2D797 /* MainReducer.swift */; }; + 87A8E15027065E9500E2D797 /* MainEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14A27065E9500E2D797 /* MainEnvironment.swift */; }; + 87A8E15127065E9500E2D797 /* MainEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14A27065E9500E2D797 /* MainEnvironment.swift */; }; + 87A8E15227065E9500E2D797 /* MainState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14B27065E9500E2D797 /* MainState.swift */; }; + 87A8E15327065E9500E2D797 /* MainState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14B27065E9500E2D797 /* MainState.swift */; }; + 87A8E15427065E9500E2D797 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14C27065E9500E2D797 /* MainView.swift */; }; + 87A8E15527065E9500E2D797 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14C27065E9500E2D797 /* MainView.swift */; }; + 87A8E15627065E9500E2D797 /* MainAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14D27065E9500E2D797 /* MainAction.swift */; }; + 87A8E15727065E9500E2D797 /* MainAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87A8E14D27065E9500E2D797 /* MainAction.swift */; }; + 87CB58D727064CF50060BF75 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58C427064CF40060BF75 /* Todo.swift */; }; + 87CB58D827064CF50060BF75 /* Todo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58C427064CF40060BF75 /* Todo.swift */; }; + 87CB58E327064CF50060BF75 /* AuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CC27064CF40060BF75 /* AuthState.swift */; }; + 87CB58E427064CF50060BF75 /* AuthState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CC27064CF40060BF75 /* AuthState.swift */; }; + 87CB58E527064CF50060BF75 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CD27064CF40060BF75 /* AuthView.swift */; }; + 87CB58E627064CF50060BF75 /* AuthView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CD27064CF40060BF75 /* AuthView.swift */; }; + 87CB58E727064CF50060BF75 /* AuthReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CE27064CF40060BF75 /* AuthReducer.swift */; }; + 87CB58E827064CF50060BF75 /* AuthReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CE27064CF40060BF75 /* AuthReducer.swift */; }; + 87CB58E927064CF50060BF75 /* AuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CF27064CF40060BF75 /* AuthAction.swift */; }; + 87CB58EA27064CF50060BF75 /* AuthAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58CF27064CF40060BF75 /* AuthAction.swift */; }; + 87CB58EB27064CF50060BF75 /* AuthEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D027064CF40060BF75 /* AuthEnvironment.swift */; }; + 87CB58EC27064CF50060BF75 /* AuthEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D027064CF40060BF75 /* AuthEnvironment.swift */; }; + 87CB58ED27064CF50060BF75 /* RootAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D227064CF40060BF75 /* RootAction.swift */; }; + 87CB58EE27064CF50060BF75 /* RootAction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D227064CF40060BF75 /* RootAction.swift */; }; + 87CB58EF27064CF50060BF75 /* RootEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D327064CF50060BF75 /* RootEnvironment.swift */; }; + 87CB58F027064CF50060BF75 /* RootEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D327064CF50060BF75 /* RootEnvironment.swift */; }; + 87CB58F127064CF50060BF75 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D427064CF50060BF75 /* RootView.swift */; }; + 87CB58F227064CF50060BF75 /* RootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D427064CF50060BF75 /* RootView.swift */; }; + 87CB58F327064CF50060BF75 /* RootState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D527064CF50060BF75 /* RootState.swift */; }; + 87CB58F427064CF50060BF75 /* RootState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D527064CF50060BF75 /* RootState.swift */; }; + 87CB58F527064CF50060BF75 /* RootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D627064CF50060BF75 /* RootReducer.swift */; }; + 87CB58F627064CF50060BF75 /* RootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87CB58D627064CF50060BF75 /* RootReducer.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8724583A270603C700A88871 /* TodosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TodosApp.swift; sourceTree = ""; }; + 8724583C270603C800A88871 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 87245841270603C800A88871 /* Todos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Todos.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 87245847270603C800A88871 /* Todos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Todos.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 87245849270603C800A88871 /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; + 878AF95427110B020066E71C /* CounterAction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterAction.swift; sourceTree = ""; }; + 878AF95527110B020066E71C /* CounterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterView.swift; sourceTree = ""; }; + 878AF95627110B020066E71C /* CounterEnvironment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterEnvironment.swift; sourceTree = ""; }; + 878AF95727110B020066E71C /* CounterReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterReducer.swift; sourceTree = ""; }; + 878AF95827110B020066E71C /* CounterState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CounterState.swift; sourceTree = ""; }; + 878AF96427110B4C0066E71C /* View+.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+.swift"; sourceTree = ""; }; + 87A8E14927065E9500E2D797 /* MainReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainReducer.swift; sourceTree = ""; }; + 87A8E14A27065E9500E2D797 /* MainEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainEnvironment.swift; sourceTree = ""; }; + 87A8E14B27065E9500E2D797 /* MainState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainState.swift; sourceTree = ""; }; + 87A8E14C27065E9500E2D797 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; + 87A8E14D27065E9500E2D797 /* MainAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainAction.swift; sourceTree = ""; }; + 87CB58C427064CF40060BF75 /* Todo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Todo.swift; sourceTree = ""; }; + 87CB58CC27064CF40060BF75 /* AuthState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthState.swift; sourceTree = ""; }; + 87CB58CD27064CF40060BF75 /* AuthView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthView.swift; sourceTree = ""; }; + 87CB58CE27064CF40060BF75 /* AuthReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthReducer.swift; sourceTree = ""; }; + 87CB58CF27064CF40060BF75 /* AuthAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthAction.swift; sourceTree = ""; }; + 87CB58D027064CF40060BF75 /* AuthEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthEnvironment.swift; sourceTree = ""; }; + 87CB58D227064CF40060BF75 /* RootAction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootAction.swift; sourceTree = ""; }; + 87CB58D327064CF50060BF75 /* RootEnvironment.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootEnvironment.swift; sourceTree = ""; }; + 87CB58D427064CF50060BF75 /* RootView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootView.swift; sourceTree = ""; }; + 87CB58D527064CF50060BF75 /* RootState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootState.swift; sourceTree = ""; }; + 87CB58D627064CF50060BF75 /* RootReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RootReducer.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 8724583E270603C800A88871 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8769538A2706078100EB9008 /* ComposableArchitecture in Frameworks */, + 875BCFB32710EAD50015E662 /* AnyRequest in Frameworks */, + 875BCFB92710EB0C0015E662 /* Json in Frameworks */, + 875BCFB62710EAEF0015E662 /* ConvertSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87245844270603C800A88871 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8769538C2706078600EB9008 /* ComposableArchitecture in Frameworks */, + 875BCFBB2710ED8E0015E662 /* AnyRequest in Frameworks */, + 875BCFBF2710ED8E0015E662 /* Json in Frameworks */, + 875BCFBD2710ED8E0015E662 /* ConvertSwift in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 87245834270603C600A88871 = { + isa = PBXGroup; + children = ( + 87245839270603C700A88871 /* Shared */, + 87245848270603C800A88871 /* macOS */, + 87245842270603C800A88871 /* Products */, + 876953882706078100EB9008 /* Frameworks */, + ); + sourceTree = ""; + }; + 87245839270603C700A88871 /* Shared */ = { + isa = PBXGroup; + children = ( + 8724583A270603C700A88871 /* TodosApp.swift */, + 878AF92E2710F90C0066E71C /* TodoApp */, + 8724583C270603C800A88871 /* Assets.xcassets */, + ); + path = Shared; + sourceTree = ""; + }; + 87245842270603C800A88871 /* Products */ = { + isa = PBXGroup; + children = ( + 87245841270603C800A88871 /* Todos.app */, + 87245847270603C800A88871 /* Todos.app */, + ); + name = Products; + sourceTree = ""; + }; + 87245848270603C800A88871 /* macOS */ = { + isa = PBXGroup; + children = ( + 87245849270603C800A88871 /* macOS.entitlements */, + ); + path = macOS; + sourceTree = ""; + }; + 876953882706078100EB9008 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 878AF92E2710F90C0066E71C /* TodoApp */ = { + isa = PBXGroup; + children = ( + 878AF96327110B3D0066E71C /* View+ */, + 87CB58C327064CF40060BF75 /* Models */, + 878AF95327110AEE0066E71C /* CounterScreen */, + 87A8E14827065E9500E2D797 /* MainScreen */, + 87CB58CB27064CF40060BF75 /* AuthScreen */, + 87CB58D127064CF40060BF75 /* RootScreen */, + ); + path = TodoApp; + sourceTree = ""; + }; + 878AF95327110AEE0066E71C /* CounterScreen */ = { + isa = PBXGroup; + children = ( + 878AF95527110B020066E71C /* CounterView.swift */, + 878AF95827110B020066E71C /* CounterState.swift */, + 878AF95427110B020066E71C /* CounterAction.swift */, + 878AF95627110B020066E71C /* CounterEnvironment.swift */, + 878AF95727110B020066E71C /* CounterReducer.swift */, + ); + path = CounterScreen; + sourceTree = ""; + }; + 878AF96327110B3D0066E71C /* View+ */ = { + isa = PBXGroup; + children = ( + 878AF96427110B4C0066E71C /* View+.swift */, + ); + path = "View+"; + sourceTree = ""; + }; + 87A8E14827065E9500E2D797 /* MainScreen */ = { + isa = PBXGroup; + children = ( + 87A8E14C27065E9500E2D797 /* MainView.swift */, + 87A8E14B27065E9500E2D797 /* MainState.swift */, + 87A8E14D27065E9500E2D797 /* MainAction.swift */, + 87A8E14A27065E9500E2D797 /* MainEnvironment.swift */, + 87A8E14927065E9500E2D797 /* MainReducer.swift */, + ); + path = MainScreen; + sourceTree = ""; + }; + 87CB58C327064CF40060BF75 /* Models */ = { + isa = PBXGroup; + children = ( + 87CB58C427064CF40060BF75 /* Todo.swift */, + ); + path = Models; + sourceTree = ""; + }; + 87CB58CB27064CF40060BF75 /* AuthScreen */ = { + isa = PBXGroup; + children = ( + 87CB58CD27064CF40060BF75 /* AuthView.swift */, + 87CB58CC27064CF40060BF75 /* AuthState.swift */, + 87CB58CF27064CF40060BF75 /* AuthAction.swift */, + 87CB58D027064CF40060BF75 /* AuthEnvironment.swift */, + 87CB58CE27064CF40060BF75 /* AuthReducer.swift */, + ); + path = AuthScreen; + sourceTree = ""; + }; + 87CB58D127064CF40060BF75 /* RootScreen */ = { + isa = PBXGroup; + children = ( + 87CB58D427064CF50060BF75 /* RootView.swift */, + 87CB58D527064CF50060BF75 /* RootState.swift */, + 87CB58D227064CF40060BF75 /* RootAction.swift */, + 87CB58D327064CF50060BF75 /* RootEnvironment.swift */, + 87CB58D627064CF50060BF75 /* RootReducer.swift */, + ); + path = RootScreen; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 87245840270603C800A88871 /* Todos (iOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87245852270603C800A88871 /* Build configuration list for PBXNativeTarget "Todos (iOS)" */; + buildPhases = ( + 8724583D270603C800A88871 /* Sources */, + 8724583E270603C800A88871 /* Frameworks */, + 8724583F270603C800A88871 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Todos (iOS)"; + packageProductDependencies = ( + 876953892706078100EB9008 /* ComposableArchitecture */, + 875BCFB22710EAD50015E662 /* AnyRequest */, + 875BCFB52710EAEF0015E662 /* ConvertSwift */, + 875BCFB82710EB0C0015E662 /* Json */, + ); + productName = "Todos (iOS)"; + productReference = 87245841270603C800A88871 /* Todos.app */; + productType = "com.apple.product-type.application"; + }; + 87245846270603C800A88871 /* Todos (macOS) */ = { + isa = PBXNativeTarget; + buildConfigurationList = 87245855270603C800A88871 /* Build configuration list for PBXNativeTarget "Todos (macOS)" */; + buildPhases = ( + 87245843270603C800A88871 /* Sources */, + 87245844270603C800A88871 /* Frameworks */, + 87245845270603C800A88871 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "Todos (macOS)"; + packageProductDependencies = ( + 8769538B2706078600EB9008 /* ComposableArchitecture */, + 875BCFBA2710ED8E0015E662 /* AnyRequest */, + 875BCFBC2710ED8E0015E662 /* ConvertSwift */, + 875BCFBE2710ED8E0015E662 /* Json */, + ); + productName = "Todos (macOS)"; + productReference = 87245847270603C800A88871 /* Todos.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 87245835270603C600A88871 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + TargetAttributes = { + 87245840270603C800A88871 = { + CreatedOnToolsVersion = 13.0; + }; + 87245846270603C800A88871 = { + CreatedOnToolsVersion = 13.0; + }; + }; + }; + buildConfigurationList = 87245838270603C600A88871 /* Build configuration list for PBXProject "Todos" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 87245834270603C600A88871; + packageReferences = ( + 875BCFB12710EAD50015E662 /* XCRemoteSwiftPackageReference "AnyRequest" */, + 875BCFB42710EAEF0015E662 /* XCRemoteSwiftPackageReference "SwiftConvert" */, + 875BCFB72710EB0C0015E662 /* XCRemoteSwiftPackageReference "Json" */, + ); + productRefGroup = 87245842270603C800A88871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 87245840270603C800A88871 /* Todos (iOS) */, + 87245846270603C800A88871 /* Todos (macOS) */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8724583F270603C800A88871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8724584E270603C800A88871 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87245845270603C800A88871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8724584F270603C800A88871 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8724583D270603C800A88871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 878AF95D27110B020066E71C /* CounterEnvironment.swift in Sources */, + 87A8E15627065E9500E2D797 /* MainAction.swift in Sources */, + 878AF96127110B020066E71C /* CounterState.swift in Sources */, + 87CB58E527064CF50060BF75 /* AuthView.swift in Sources */, + 878AF96527110B4C0066E71C /* View+.swift in Sources */, + 87A8E15227065E9500E2D797 /* MainState.swift in Sources */, + 87A8E14E27065E9500E2D797 /* MainReducer.swift in Sources */, + 87CB58D727064CF50060BF75 /* Todo.swift in Sources */, + 87CB58F327064CF50060BF75 /* RootState.swift in Sources */, + 87CB58F127064CF50060BF75 /* RootView.swift in Sources */, + 87CB58E727064CF50060BF75 /* AuthReducer.swift in Sources */, + 878AF95B27110B020066E71C /* CounterView.swift in Sources */, + 87CB58ED27064CF50060BF75 /* RootAction.swift in Sources */, + 87CB58F527064CF50060BF75 /* RootReducer.swift in Sources */, + 87A8E15427065E9500E2D797 /* MainView.swift in Sources */, + 87CB58EB27064CF50060BF75 /* AuthEnvironment.swift in Sources */, + 878AF95927110B020066E71C /* CounterAction.swift in Sources */, + 87CB58EF27064CF50060BF75 /* RootEnvironment.swift in Sources */, + 87A8E15027065E9500E2D797 /* MainEnvironment.swift in Sources */, + 87CB58E927064CF50060BF75 /* AuthAction.swift in Sources */, + 8724584A270603C800A88871 /* TodosApp.swift in Sources */, + 878AF95F27110B020066E71C /* CounterReducer.swift in Sources */, + 87CB58E327064CF50060BF75 /* AuthState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 87245843270603C800A88871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 878AF95E27110B020066E71C /* CounterEnvironment.swift in Sources */, + 87A8E15727065E9500E2D797 /* MainAction.swift in Sources */, + 878AF96227110B020066E71C /* CounterState.swift in Sources */, + 87CB58E627064CF50060BF75 /* AuthView.swift in Sources */, + 878AF96627110B4C0066E71C /* View+.swift in Sources */, + 87A8E15327065E9500E2D797 /* MainState.swift in Sources */, + 87A8E14F27065E9500E2D797 /* MainReducer.swift in Sources */, + 87CB58D827064CF50060BF75 /* Todo.swift in Sources */, + 87CB58F427064CF50060BF75 /* RootState.swift in Sources */, + 87CB58F227064CF50060BF75 /* RootView.swift in Sources */, + 87CB58E827064CF50060BF75 /* AuthReducer.swift in Sources */, + 878AF95C27110B020066E71C /* CounterView.swift in Sources */, + 87CB58EE27064CF50060BF75 /* RootAction.swift in Sources */, + 87CB58F627064CF50060BF75 /* RootReducer.swift in Sources */, + 87A8E15527065E9500E2D797 /* MainView.swift in Sources */, + 87CB58EC27064CF50060BF75 /* AuthEnvironment.swift in Sources */, + 878AF95A27110B020066E71C /* CounterAction.swift in Sources */, + 87CB58F027064CF50060BF75 /* RootEnvironment.swift in Sources */, + 87A8E15127065E9500E2D797 /* MainEnvironment.swift in Sources */, + 87CB58EA27064CF50060BF75 /* AuthAction.swift in Sources */, + 8724584B270603C800A88871 /* TodosApp.swift in Sources */, + 878AF96027110B020066E71C /* CounterReducer.swift in Sources */, + 87CB58E427064CF50060BF75 /* AuthState.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 87245850270603C800A88871 /* 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++17"; + 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; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 87245851270603C800A88871 /* 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++17"; + 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; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 87245853270603C800A88871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstack.swift.Todos; + PRODUCT_NAME = Todos; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 87245854270603C800A88871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstack.swift.Todos; + PRODUCT_NAME = Todos; + SDKROOT = iphoneos; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 87245856270603C800A88871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstack.swift.Todos; + PRODUCT_NAME = Todos; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 87245857270603C800A88871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = macOS/macOS.entitlements; + CODE_SIGN_IDENTITY = "-"; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 2H5PN3F9B6; + ENABLE_HARDENED_RUNTIME = YES; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 11.0; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = mike.fullstack.swift.Todos; + PRODUCT_NAME = Todos; + SDKROOT = macosx; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 87245838270603C600A88871 /* Build configuration list for PBXProject "Todos" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87245850270603C800A88871 /* Debug */, + 87245851270603C800A88871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 87245852270603C800A88871 /* Build configuration list for PBXNativeTarget "Todos (iOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87245853270603C800A88871 /* Debug */, + 87245854270603C800A88871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 87245855270603C800A88871 /* Build configuration list for PBXNativeTarget "Todos (macOS)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 87245856270603C800A88871 /* Debug */, + 87245857270603C800A88871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 875BCFB12710EAD50015E662 /* XCRemoteSwiftPackageReference "AnyRequest" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/FullStack-Swift/AnyRequest"; + requirement = { + branch = main; + kind = branch; + }; + }; + 875BCFB42710EAEF0015E662 /* XCRemoteSwiftPackageReference "SwiftConvert" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/FullStack-Swift/SwiftConvert"; + requirement = { + branch = main; + kind = branch; + }; + }; + 875BCFB72710EB0C0015E662 /* XCRemoteSwiftPackageReference "Json" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/FullStack-Swift/Json"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 875BCFB22710EAD50015E662 /* AnyRequest */ = { + isa = XCSwiftPackageProductDependency; + package = 875BCFB12710EAD50015E662 /* XCRemoteSwiftPackageReference "AnyRequest" */; + productName = AnyRequest; + }; + 875BCFB52710EAEF0015E662 /* ConvertSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 875BCFB42710EAEF0015E662 /* XCRemoteSwiftPackageReference "SwiftConvert" */; + productName = ConvertSwift; + }; + 875BCFB82710EB0C0015E662 /* Json */ = { + isa = XCSwiftPackageProductDependency; + package = 875BCFB72710EB0C0015E662 /* XCRemoteSwiftPackageReference "Json" */; + productName = Json; + }; + 875BCFBA2710ED8E0015E662 /* AnyRequest */ = { + isa = XCSwiftPackageProductDependency; + package = 875BCFB12710EAD50015E662 /* XCRemoteSwiftPackageReference "AnyRequest" */; + productName = AnyRequest; + }; + 875BCFBC2710ED8E0015E662 /* ConvertSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 875BCFB42710EAEF0015E662 /* XCRemoteSwiftPackageReference "SwiftConvert" */; + productName = ConvertSwift; + }; + 875BCFBE2710ED8E0015E662 /* Json */ = { + isa = XCSwiftPackageProductDependency; + package = 875BCFB72710EB0C0015E662 /* XCRemoteSwiftPackageReference "Json" */; + productName = Json; + }; + 876953892706078100EB9008 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; + 8769538B2706078600EB9008 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 87245835270603C600A88871 /* Project object */; +} diff --git a/Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme b/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme new file mode 100644 index 0000000..80a7221 --- /dev/null +++ b/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme b/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme new file mode 100644 index 0000000..caa2a17 --- /dev/null +++ b/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Examples/Todos/macOS/macOS.entitlements b/Examples/Todos/macOS/macOS.entitlements new file mode 100644 index 0000000..40b639e --- /dev/null +++ b/Examples/Todos/macOS/macOS.entitlements @@ -0,0 +1,14 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + com.apple.security.network.server + + + diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..4b1173d --- /dev/null +++ b/Package.resolved @@ -0,0 +1,88 @@ +{ + "object": { + "pins": [ + { + "package": "combine-schedulers", + "repositoryURL": "https://github.com/pointfreeco/combine-schedulers", + "state": { + "branch": null, + "revision": "4cf088c29a20f52be0f2ca54992b492c54e0076b", + "version": "0.5.3" + } + }, + { + "package": "ReactiveSwift", + "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", + "state": { + "branch": null, + "revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", + "version": "6.7.0" + } + }, + { + "package": "swift-argument-parser", + "repositoryURL": "https://github.com/apple/swift-argument-parser", + "state": { + "branch": null, + "revision": "6b2aa2748a7881eebb9f84fb10c01293e15b52ca", + "version": "0.5.0" + } + }, + { + "package": "Benchmark", + "repositoryURL": "https://github.com/google/swift-benchmark", + "state": { + "branch": null, + "revision": "a0564bf88df5f94eec81348a2f089494c6b28d80", + "version": "0.1.1" + } + }, + { + "package": "swift-case-paths", + "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", + "state": { + "branch": null, + "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", + "version": "0.7.0" + } + }, + { + "package": "swift-collections", + "repositoryURL": "https://github.com/apple/swift-collections", + "state": { + "branch": null, + "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", + "version": "1.0.1" + } + }, + { + "package": "swift-custom-dump", + "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", + "state": { + "branch": null, + "revision": "c2dd2c64b753dda592f5619303e02f741cd3e862", + "version": "0.2.0" + } + }, + { + "package": "swift-identified-collections", + "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", + "state": { + "branch": null, + "revision": "c8e6a40209650ab619853cd4ce89a0aa51792754", + "version": "0.3.0" + } + }, + { + "package": "xctest-dynamic-overlay", + "repositoryURL": "https://github.com/pointfreeco/xctest-dynamic-overlay", + "state": { + "branch": null, + "revision": "50a70a9d3583fe228ce672e8923010c8df2deddd", + "version": "0.2.1" + } + } + ] + }, + "version": 1 +} diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..ecff37f --- /dev/null +++ b/Package.swift @@ -0,0 +1,50 @@ +// swift-tools-version:5.5 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "reactiveswift-composable-architecture", + platforms: [ + .iOS(.v13), + .macOS(.v10_15), + .tvOS(.v13), + .watchOS(.v6), + ], + products: [ + .library( + name: "ComposableArchitecture", + targets: ["ComposableArchitecture"]), + ], + dependencies: [ + .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "6.7.0"), + .package(name: "Benchmark", url: "https://github.com/google/swift-benchmark", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/combine-schedulers", from: "0.5.0"), + .package(url: "https://github.com/pointfreeco/swift-case-paths", from: "0.4.0"), + .package(url: "https://github.com/pointfreeco/swift-custom-dump", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/swift-identified-collections", from: "0.1.0"), + .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "0.2.0"), + ], + targets: [ + .target( + name: "ComposableArchitecture", + dependencies: [ + "ReactiveSwift", + .product(name: "CasePaths", package: "swift-case-paths"), + .product(name: "CombineSchedulers", package: "combine-schedulers"), + .product(name: "CustomDump", package: "swift-custom-dump"), + .product(name: "IdentifiedCollections", package: "swift-identified-collections"), + .product(name: "XCTestDynamicOverlay", package: "xctest-dynamic-overlay"), + ]), + .testTarget( + name: "ComposableArchitectureTests", + dependencies: ["ComposableArchitecture"]), + .executableTarget( + name: "swift-composable-architecture-benchmark", + dependencies: [ + "ComposableArchitecture", + .product(name: "Benchmark", package: "Benchmark"), + ] + ), + ] +) diff --git a/README.md b/README.md new file mode 100644 index 0000000..23b06b4 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# reactiveswift-composable-architecture + +A description of this package. + +## Combine + +https://github.com/pointfreeco/swift-composable-architecture + +## RxSwift + +https://github.com/FullStack-Swift/rxswift-composable-architecture + +## ReactiveSwift + +https://github.com/FullStack-Swift/reactiveswift-composable-architecture + +## Example + +https://github.com/FullStack-Swift/TodoFullStackSwift diff --git a/Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift b/Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift new file mode 100644 index 0000000..6e9d68d --- /dev/null +++ b/Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift @@ -0,0 +1,28 @@ +#if canImport(Combine) +import Combine +import ReactiveSwift + +@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) +public extension Publisher { + func eraseToEffect() -> Effect { + SignalProducer { observer, disposable in + let cancellable = self.sink( + receiveCompletion: { completion in + switch completion { + case .finished: + observer.sendCompleted() + case .failure(let error): + observer.send(error: error) + } + }, + receiveValue: { value in + observer.send(value: value) + }) + + disposable.observeEnded { + cancellable.cancel() + } + } + } +} +#endif diff --git a/Sources/ComposableArchitecture/Beta/Concurrency.swift b/Sources/ComposableArchitecture/Beta/Concurrency.swift new file mode 100644 index 0000000..178b5cd --- /dev/null +++ b/Sources/ComposableArchitecture/Beta/Concurrency.swift @@ -0,0 +1,103 @@ +import ReactiveSwift +import SwiftUI +import Combine + +#if compiler(>=5.5) && canImport(_Concurrency) +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension Effect { + public static func task( + priority: TaskPriority? = nil, + operation: @escaping @Sendable () async -> Value + ) -> Self where Error == Never { + var task: Task? + return .future { callback in + task = Task(priority: priority) { + guard !Task.isCancelled else { return } + let output = await operation() + guard !Task.isCancelled else { return } + callback(.success(output)) + } + } + .on(disposed: { task?.cancel() }) + } + + public static func task( + priority: TaskPriority? = nil, + operation: @escaping @Sendable () async throws -> Value + ) -> Self where Error == Swift.Error { + deferred { + var task: Task<(), Never>? + let producer = SignalProducer { observer, lifetime in + task = Task(priority: priority) { + do { + try Task.checkCancellation() + let output = try await operation() + try Task.checkCancellation() + observer.send(value: output) + observer.sendCompleted() + } catch is CancellationError { + observer.sendCompleted() + } catch { + observer.send(error: error) + } + } + } + + return producer.on(disposed: task?.cancel) + } + } +} + +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension ViewStore { + public func send( + _ action: Action, + while predicate: @escaping (State) -> Bool + ) async { + self.send(action) + await self.suspend(while: predicate) + } + +#if canImport(SwiftUI) + public func send( + _ action: Action, + animation: Animation?, + while predicate: @escaping (State) -> Bool + ) async { + withAnimation(animation) { self.send(action) } + await self.suspend(while: predicate) + } +#endif + public func suspend(while predicate: @escaping (State) -> Bool) async { + let cancellable = Box(wrappedValue: nil) + try? await withTaskCancellationHandler( + handler: { cancellable.wrappedValue?.dispose() }, + operation: { + try Task.checkCancellation() + try await withUnsafeThrowingContinuation { + (continuation: UnsafeContinuation) in + guard !Task.isCancelled else { + continuation.resume(throwing: CancellationError()) + return + } + cancellable.wrappedValue = self.publisher.producer + .filter { !predicate($0) } + .take(first: 1) + .startWithValues { _ in + continuation.resume() + _ = cancellable + } + } + } + ) + } +} + +private class Box { + var wrappedValue: Value + + init(wrappedValue: Value) { + self.wrappedValue = wrappedValue + } +} +#endif diff --git a/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift b/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift new file mode 100644 index 0000000..033f0b3 --- /dev/null +++ b/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift @@ -0,0 +1,111 @@ +import CasePaths +import Dispatch + +public enum ActionFormat { + case labelsOnly + case prettyPrint +} + +extension Reducer { + public func debug( + _ prefix: String = "", + actionFormat: ActionFormat = .prettyPrint, + environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in + DebugEnvironment() + } + ) -> Reducer { + self.debug( + prefix, + state: { $0 }, + action: .self, + actionFormat: actionFormat, + environment: toDebugEnvironment + ) + } + + public func debugActions( + _ prefix: String = "", + actionFormat: ActionFormat = .prettyPrint, + environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in + DebugEnvironment() + } + ) -> Reducer { + self.debug( + prefix, + state: { _ in () }, + action: .self, + actionFormat: actionFormat, + environment: toDebugEnvironment + ) + } + + public func debug( + _ prefix: String = "", + state toLocalState: @escaping (State) -> LocalState, + action toLocalAction: CasePath, + actionFormat: ActionFormat = .prettyPrint, + environment toDebugEnvironment: @escaping (Environment) -> DebugEnvironment = { _ in + DebugEnvironment() + } + ) -> Reducer { +#if DEBUG + return .init { state, action, environment in + let previousState = toLocalState(state) + let effects = self.run(&state, action, environment) + guard let localAction = toLocalAction.extract(from: action) else { return effects } + let nextState = toLocalState(state) + let debugEnvironment = toDebugEnvironment(environment) + return Effect.merge( + Effect.fireAndForget { + debugEnvironment.queue.async { + var actionOutput = "" + if actionFormat == .prettyPrint { + customDump(localAction, to: &actionOutput, indent: 2) + } else { + actionOutput.write(debugCaseOutput(localAction).indent(by: 2)) + } + let stateOutput = + LocalState.self == Void.self + ? "" + : diff(previousState, nextState).map { "\($0)\n" } ?? " (No state changes)\n" + debugEnvironment.printer( + """ + \(prefix.isEmpty ? "" : "\(prefix): ")received action: + \(actionOutput) + \(stateOutput) + """ + ) + } + }, + effects + ) + } +#else + return self +#endif + } +} + +public struct DebugEnvironment { + public var printer: (String) -> Void + public var queue: DispatchQueue + + public init( + printer: @escaping (String) -> Void = { print($0) }, + queue: DispatchQueue + ) { + self.printer = printer + self.queue = queue + } + + public init( + printer: @escaping (String) -> Void = { print($0) } + ) { + self.init(printer: printer, queue: _queue) + } +} + +private let _queue = DispatchQueue( + label: "ComposableArchitecture.DebugEnvironment", + qos: .background +) diff --git a/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift b/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift new file mode 100644 index 0000000..5c73b71 --- /dev/null +++ b/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift @@ -0,0 +1,95 @@ +#if canImport(os) +import os.signpost + +extension Reducer { + @available(iOS 12.0, *) + public func signpost( + _ prefix: String = "", + log: OSLog = OSLog( + subsystem: "co.pointfree.composable-architecture", + category: "Reducer Instrumentation" + ) + ) -> Self { + guard log.signpostsEnabled else { return self } + + // NB: Prevent rendering as "N/A" in Instruments + let zeroWidthSpace = "\u{200B}" + + let prefix = prefix.isEmpty ? zeroWidthSpace : "[\(prefix)] " + + return Self { state, action, environment in + var actionOutput: String! + if log.signpostsEnabled { + actionOutput = debugCaseOutput(action) + os_signpost(.begin, log: log, name: "Action", "%s%s", prefix, actionOutput) + } + let effects = self.run(&state, action, environment) + if log.signpostsEnabled { + os_signpost(.end, log: log, name: "Action") + return effects + .effectSignpost(prefix, log: log, actionOutput: actionOutput) + } + return effects + } + } +} + +extension Effect where Error == Never { + @available(iOS 12.0, *) + func effectSignpost( + _ prefix: String, + log: OSLog, + actionOutput: String + ) -> Effect { + let sid = OSSignpostID(log: log) + + return self.on( + starting: { + os_signpost( + .begin, log: log, name: "Effect", signpostID: sid, "%sStarted from %s", prefix, + actionOutput + ) + }, + completed: { + os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sFinished", prefix) + }, + disposed: { + os_signpost(.end, log: log, name: "Effect", signpostID: sid, "%sCancelled", prefix) + }, + value: { value in + os_signpost( + .event, log: log, name: "Effect Output", "%sOutput from %s", prefix, actionOutput + ) + }) + } +} +#endif + +func debugCaseOutput(_ value: Any) -> String { + func debugCaseOutputHelp(_ value: Any) -> String { + let mirror = Mirror(reflecting: value) + switch mirror.displayStyle { + case .enum: + guard let child = mirror.children.first else { + let childOutput = "\(value)" + return childOutput == "\(type(of: value))" ? "" : ".\(childOutput)" + } + let childOutput = debugCaseOutputHelp(child.value) + return ".\(child.label ?? "")\(childOutput.isEmpty ? "" : "(\(childOutput))")" + case .tuple: + return mirror.children.map { label, value in + let childOutput = debugCaseOutputHelp(value) + return "\(label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")" + } + .joined(separator: ", ") + default: + return "" + } + } + + return "\(type(of: value))\(debugCaseOutputHelp(value))" +} + +private func isUnlabeledArgument(_ label: String) -> Bool { + label.firstIndex(where: { $0 != "." && !$0.isNumber }) == nil +} diff --git a/Sources/ComposableArchitecture/Effect.swift b/Sources/ComposableArchitecture/Effect.swift new file mode 100644 index 0000000..752a337 --- /dev/null +++ b/Sources/ComposableArchitecture/Effect.swift @@ -0,0 +1,93 @@ +import Foundation +import ReactiveSwift + +public typealias Effect = SignalProducer + +extension Effect { + public static var none: Effect { + .empty + } + + public static func fireAndForget(_ work: @escaping () -> Void) -> Effect { + .deferred { () -> SignalProducer in + work() + return .empty + } + } + + public static func concatenate(_ effects: Effect...) -> Effect { + .concatenate(effects) + } + + public static func concatenate( + _ effects: C + ) -> Effect where C.Element == Effect { + guard let first = effects.first else { return .none } + return effects + .dropFirst() + .reduce(into: first) { effects, effect in + effects = effects.concat(effect) + } + } + + public static func deferred(_ createProducer: @escaping () -> SignalProducer) + -> SignalProducer + { + Effect(value: ()) + .flatMap(.merge, createProducer) + } + + public static func future( + _ attemptToFulfill: @escaping (@escaping (Result) -> Void) -> Void + ) -> Effect { + SignalProducer { observer, _ in + attemptToFulfill { result in + switch result { + case let .success(value): + observer.send(value: value) + observer.sendCompleted() + case let .failure(error): + observer.send(error: error) + } + } + } + } + + public func catchToEffect() -> Effect, Never> { + self.map(Result.success) + .flatMapError { Effect, Never>(value: Result.failure($0)) } + } + + public func catchToEffect( + _ transform: @escaping (Result) -> T + ) -> Effect { + self + .map { transform(.success($0)) } + .flatMapError { Effect(value: transform(.failure($0))) } + } + + public func fireAndForget( + outputType: NewValue.Type = NewValue.self, + failureType: NewError.Type = NewError.self + ) -> Effect { + self.flatMapError { _ in .empty } + .flatMap(.latest) { _ in + .empty + } + } + + public func eraseToEffect() -> Self { + self + } +} + +extension Effect where Self.Error == Never { + @discardableResult + public func assign(to keyPath: ReferenceWritableKeyPath, on object: Root) + -> Disposable + { + self.startWithValues { value in + object[keyPath: keyPath] = value + } + } +} diff --git a/Sources/ComposableArchitecture/Effects/Cancellation.swift b/Sources/ComposableArchitecture/Effects/Cancellation.swift new file mode 100644 index 0000000..efd474c --- /dev/null +++ b/Sources/ComposableArchitecture/Effects/Cancellation.swift @@ -0,0 +1,81 @@ +import Foundation +import ReactiveSwift + +extension AnyDisposable: Hashable { + public static func == (lhs: AnyDisposable, rhs: AnyDisposable) -> Bool { + return ObjectIdentifier(lhs) == ObjectIdentifier(rhs) + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } +} + +extension Effect { + public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Effect { + let effect = Effect.deferred { () -> SignalProducer in + cancellablesLock.lock() + defer { cancellablesLock.unlock() } + + let subject = Signal.pipe() + + var values: [Value] = [] + var isCaching = true + + let disposable = + self + .on(value: { + guard isCaching else { return } + values.append($0) + }) + .start(subject.input) + + var cancellationDisposable: AnyDisposable! + cancellationDisposable = AnyDisposable { + cancellablesLock.sync { + subject.input.sendCompleted() + disposable.dispose() + cancellationCancellables[id]?.remove(cancellationDisposable) + if cancellationCancellables[id]?.isEmpty == .some(true) { + cancellationCancellables[id] = nil + } + } + } + + cancellationCancellables[id, default: []].insert( + cancellationDisposable + ) + + return SignalProducer(values) + .concat(subject.output.producer) + .on( + started: { isCaching = false }, + completed: cancellationDisposable.dispose, + interrupted: cancellationDisposable.dispose, + terminated: cancellationDisposable.dispose, + disposed: cancellationDisposable.dispose + ) + } + + return cancelInFlight ? .concatenate(.cancel(id: id), effect) : effect + } + + public static func cancel(id: AnyHashable) -> Effect { + return .fireAndForget { + cancellablesLock.sync { + cancellationCancellables[id]?.forEach { $0.dispose() } + } + } + } + + public static func cancel(ids: AnyHashable...) -> Effect { + .cancel(ids: ids) + } + + public static func cancel(ids: [AnyHashable]) -> Effect { + .merge(ids.map(Effect.cancel(id:))) + } +} + +var cancellationCancellables: [AnyHashable: Set] = [:] +let cancellablesLock = NSRecursiveLock() diff --git a/Sources/ComposableArchitecture/Effects/Debouncing.swift b/Sources/ComposableArchitecture/Effects/Debouncing.swift new file mode 100644 index 0000000..e5d04fe --- /dev/null +++ b/Sources/ComposableArchitecture/Effects/Debouncing.swift @@ -0,0 +1,16 @@ +import Foundation +import ReactiveSwift + +extension Effect { + public func debounce( + id: AnyHashable, + for dueTime: TimeInterval, + scheduler: DateScheduler + ) -> Effect { + Effect.init(value: ()) + .promoteError(Error.self) + .delay(dueTime, on: scheduler) + .flatMap(.latest) { self.observe(on: scheduler) } + .cancellable(id: id, cancelInFlight: true) + } +} diff --git a/Sources/ComposableArchitecture/Effects/Deferring.swift b/Sources/ComposableArchitecture/Effects/Deferring.swift new file mode 100644 index 0000000..b5eca46 --- /dev/null +++ b/Sources/ComposableArchitecture/Effects/Deferring.swift @@ -0,0 +1,13 @@ +import Foundation +import ReactiveSwift + +extension Effect { + public func deferred( + for dueTime: TimeInterval, + scheduler: DateScheduler + ) -> Effect { + SignalProducer(value: ()) + .delay(dueTime, on: scheduler) + .flatMap(.latest) { self.observe(on: scheduler) } + } +} diff --git a/Sources/ComposableArchitecture/Effects/Throttling.swift b/Sources/ComposableArchitecture/Effects/Throttling.swift new file mode 100644 index 0000000..a54d2d2 --- /dev/null +++ b/Sources/ComposableArchitecture/Effects/Throttling.swift @@ -0,0 +1,55 @@ +import Dispatch +import Foundation +import ReactiveSwift + +extension Effect { + public func throttle( + id: AnyHashable, + for interval: TimeInterval, + scheduler: DateScheduler, + latest: Bool + ) -> Effect { + self.observe(on: scheduler) + .flatMap(.latest) { value -> Effect in + throttleLock.lock() + defer { throttleLock.unlock() } + + guard let throttleTime = throttleTimes[id] as! Date? else { + throttleTimes[id] = scheduler.currentDate + throttleValues[id] = nil + return Effect(value: value) + } + + let value = latest ? value : (throttleValues[id] as! Value? ?? value) + throttleValues[id] = value + + guard + scheduler.currentDate.timeIntervalSince1970 - throttleTime.timeIntervalSince1970 + < interval + else { + throttleTimes[id] = scheduler.currentDate + throttleValues[id] = nil + return Effect(value: value) + } + + return Effect(value: value) + .delay( + throttleTime.addingTimeInterval(interval).timeIntervalSince1970 + - scheduler.currentDate.timeIntervalSince1970, + on: scheduler + ).on( + value: { _ in + throttleLock.sync { + throttleTimes[id] = scheduler.currentDate + throttleValues[id] = nil + } + } + ) + } + .cancellable(id: id, cancelInFlight: true) + } +} + +var throttleTimes: [AnyHashable: Any] = [:] +var throttleValues: [AnyHashable: Any] = [:] +let throttleLock = NSRecursiveLock() diff --git a/Sources/ComposableArchitecture/Effects/Timer.swift b/Sources/ComposableArchitecture/Effects/Timer.swift new file mode 100644 index 0000000..5d11df9 --- /dev/null +++ b/Sources/ComposableArchitecture/Effects/Timer.swift @@ -0,0 +1,16 @@ +import Foundation +import ReactiveSwift + +extension Effect where Value == Date, Error == Never { + public static func timer( + id: AnyHashable, + every interval: DispatchTimeInterval, + tolerance: DispatchTimeInterval? = nil, + on scheduler: DateScheduler + ) -> Effect { + return SignalProducer.timer( + interval: interval, on: scheduler, leeway: tolerance ?? .seconds(.max) + ) + .cancellable(id: id, cancelInFlight: true) + } +} diff --git a/Sources/ComposableArchitecture/Internal/Breakpoint.swift b/Sources/ComposableArchitecture/Internal/Breakpoint.swift new file mode 100644 index 0000000..9fcaead --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Breakpoint.swift @@ -0,0 +1,32 @@ +#if canImport(Darwin) + import Darwin +#endif +@inline(__always) func breakpoint(_ message: @autoclosure () -> String = "") { + #if DEBUG && canImport(Darwin) + // https://github.com/bitstadium/HockeySDK-iOS/blob/c6e8d1e940299bec0c0585b1f7b86baf3b17fc82/Classes/BITHockeyHelper.m#L346-L370 + var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()] + var info: kinfo_proc = kinfo_proc() + var info_size = MemoryLayout.size + + let isDebuggerAttached = name.withUnsafeMutableBytes { + $0.bindMemory(to: Int32.self).baseAddress + .map { + sysctl($0, 4, &info, &info_size, nil, 0) != -1 && info.kp_proc.p_flag & P_TRACED != 0 + } + ?? false + } + + if isDebuggerAttached { + fputs( + """ + \(message()) + + Caught debug breakpoint. Type "continue" ("c") to resume execution. + + """, + stderr + ) + raise(SIGTRAP) + } + #endif +} diff --git a/Sources/ComposableArchitecture/Internal/Debug.swift b/Sources/ComposableArchitecture/Internal/Debug.swift new file mode 100644 index 0000000..ef7a3b2 --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Debug.swift @@ -0,0 +1,216 @@ +import Foundation + +#if os(Linux) || os(Android) + import let CDispatch.NSEC_PER_USEC + import let CDispatch.NSEC_PER_SEC +#endif + +func debugOutput(_ value: Any, indent: Int = 0) -> String { + var visitedItems: Set = [] + + func debugOutputHelp(_ value: Any, indent: Int = 0) -> String { + let mirror = Mirror(reflecting: value) + switch (value, mirror.displayStyle) { + case let (value as CustomDebugOutputConvertible, _): + return value.debugOutput.indent(by: indent) + case (_, .collection?): + return """ + [ + \(mirror.children.map { "\(debugOutput($0.value, indent: 2)),\n" }.joined())] + """ + .indent(by: indent) + + case (_, .dictionary?): + let pairs = mirror.children.map { label, value -> String in + let pair = value as! (key: AnyHashable, value: Any) + return + "\("\(debugOutputHelp(pair.key.base)): \(debugOutputHelp(pair.value)),".indent(by: 2))\n" + } + return """ + [ + \(pairs.sorted().joined())] + """ + .indent(by: indent) + + case (_, .set?): + return """ + Set([ + \(mirror.children.map { "\(debugOutputHelp($0.value, indent: 2)),\n" }.sorted().joined())]) + """ + .indent(by: indent) + + case (_, .optional?): + return mirror.children.isEmpty + ? "nil".indent(by: indent) + : debugOutputHelp(mirror.children.first!.value, indent: indent) + + case (_, .enum?) where !mirror.children.isEmpty: + let child = mirror.children.first! + let childMirror = Mirror(reflecting: child.value) + let elements = + childMirror.displayStyle != .tuple + ? debugOutputHelp(child.value, indent: 2) + : childMirror.children.map { child -> String in + let label = child.label! + return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutputHelp(child.value))" + } + .joined(separator: ",\n") + .indent(by: 2) + return """ + \(mirror.subjectType).\(child.label!)( + \(elements) + ) + """ + .indent(by: indent) + + case (_, .enum?): + return """ + \(mirror.subjectType).\(value) + """ + .indent(by: indent) + + case (_, .struct?) where !mirror.children.isEmpty: + let elements = mirror.children + .map { "\($0.label.map { "\($0): " } ?? "")\(debugOutputHelp($0.value))".indent(by: 2) } + .joined(separator: ",\n") + return """ + \(mirror.subjectType)( + \(elements) + ) + """ + .indent(by: indent) + + case let (value as AnyObject, .class?) + where !mirror.children.isEmpty && !visitedItems.contains(ObjectIdentifier(value)): + visitedItems.insert(ObjectIdentifier(value)) + let elements = mirror.children + .map { "\($0.label.map { "\($0): " } ?? "")\(debugOutputHelp($0.value))".indent(by: 2) } + .joined(separator: ",\n") + return """ + \(mirror.subjectType)( + \(elements) + ) + """ + .indent(by: indent) + + case let (value as AnyObject, .class?) + where !mirror.children.isEmpty && visitedItems.contains(ObjectIdentifier(value)): + return "\(mirror.subjectType)(↩︎)" + + case let (value as CustomStringConvertible, .class?): + return value.description + .replacingOccurrences( + of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression + ) + .indent(by: indent) + + case let (value as CustomDebugStringConvertible, _): + return value.debugDescription + .replacingOccurrences( + of: #"^<([^:]+): 0x[^>]+>$"#, with: "$1()", options: .regularExpression + ) + .indent(by: indent) + + case let (value as CustomStringConvertible, _): + return value.description + .indent(by: indent) + + case (_, .struct?), (_, .class?): + return "\(mirror.subjectType)()" + .indent(by: indent) + + case (_, .tuple?) where mirror.children.isEmpty: + return "()" + .indent(by: indent) + + case (_, .tuple?): + let elements = mirror.children.map { child -> String in + let label = child.label! + return "\(label.hasPrefix(".") ? "" : "\(label): ")\(debugOutputHelp(child.value))" + .indent(by: 2) + } + return """ + ( + \(elements.joined(separator: ",\n")) + ) + """ + .indent(by: indent) + + case (_, nil): + return "\(value)" + .indent(by: indent) + + @unknown default: + return "\(value)" + .indent(by: indent) + } + } + + return debugOutputHelp(value, indent: indent) +} + +func debugDiff(_ before: T, _ after: T, printer: (T) -> String = { debugOutput($0) }) -> String? +{ + diff(printer(before), printer(after)) +} + +extension String { + func indent(by indent: Int) -> String { + let indentation = String(repeating: " ", count: indent) + return indentation + self.replacingOccurrences(of: "\n", with: "\n\(indentation)") + } +} + +public protocol CustomDebugOutputConvertible { + var debugOutput: String { get } +} + +extension Date: CustomDebugOutputConvertible { + public var debugOutput: String { + dateFormatter.string(from: self) + } +} + +private let dateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.timeZone = TimeZone(identifier: "UTC")! + return formatter +}() + +extension RunLoop: CustomDebugOutputConvertible { + public var debugOutput: String { + switch self { + case .main: return "RunLoop.main" + default: return "RunLoop()" + } + } +} + +extension URL: CustomDebugOutputConvertible { + public var debugOutput: String { + self.absoluteString + } +} + +#if DEBUG + #if canImport(Speech) + import Speech + @available(OSX 10.15, *) + extension SFSpeechRecognizerAuthorizationStatus: CustomDebugOutputConvertible { + public var debugOutput: String { + switch self { + case .notDetermined: + return "notDetermined" + case .denied: + return "denied" + case .restricted: + return "restricted" + case .authorized: + return "authorized" + @unknown default: + return "unknown" + } + } + } + #endif +#endif diff --git a/Sources/ComposableArchitecture/Internal/Deprecations.swift b/Sources/ComposableArchitecture/Internal/Deprecations.swift new file mode 100644 index 0000000..f9b4b78 --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Deprecations.swift @@ -0,0 +1,415 @@ +#if canImport(SwiftUI) + import CasePaths + + import SwiftUI + + // NB: Deprecated after 0.25.0: + + #if compiler(>=5.4) + extension BindingAction { + @available( + *, deprecated, + message: + "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState', and accessed via key paths to that 'BindableState', like '\\.$value'" + ) + public static func set( + _ keyPath: WritableKeyPath, + _ value: Value + ) -> Self + where Value: Equatable { + .init( + keyPath: keyPath, + set: { $0[keyPath: keyPath] = value }, + value: value, + valueIsEqualTo: { $0 as? Value == value } + ) + } + + @available( + *, deprecated, + message: + "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState', and accessed via key paths to that 'BindableState', like '\\.$value'" + ) + public static func ~= ( + keyPath: WritableKeyPath, + bindingAction: Self + ) -> Bool { + keyPath == bindingAction.keyPath + } + } + + extension Reducer { + @available( + *, deprecated, + message: + "'Reducer.binding()' no longer takes an explicit extract function and instead the reducer's 'Action' type must conform to 'BindableAction'" + ) + public func binding(action toBindingAction: @escaping (Action) -> BindingAction?) + -> Self + { + Self { state, action, environment in + toBindingAction(action)?.set(&state) + return self.run(&state, action, environment) + } + } + } + + #if canImport(SwiftUI) + extension ViewStore { + @available( + *, deprecated, + message: + "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState'. Bindings are now derived via dynamic member lookup to that 'BindableState' (for example, 'viewStore.$value'). For dynamic member lookup to be available, the view store's 'Action' type must also conform to 'BindableAction'." + ) + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func binding( + keyPath: WritableKeyPath, + send action: @escaping (BindingAction) -> Action + ) -> Binding + where LocalState: Equatable { + self.binding( + get: { $0[keyPath: keyPath] }, + send: { action(.set(keyPath, $0)) } + ) + } + } + #endif + #else + extension BindingAction { + @available( + *, deprecated, + message: + "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState', and accessed via key paths to that 'BindableState', like '\\.$value'. Upgrade to Xcode 12.5 or greater for access to 'BindableState'." + ) + public static func set( + _ keyPath: WritableKeyPath, + _ value: Value + ) -> Self + where Value: Equatable { + .init( + keyPath: keyPath, + set: { $0[keyPath: keyPath] = value }, + value: value, + valueIsEqualTo: { $0 as? Value == value } + ) + } + + @available( + *, deprecated, + message: + "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState', and accessed via key paths to that 'BindableState', like '\\.$value'. Upgrade to Xcode 12.5 or greater for access to 'BindableState'." + ) + public static func ~= ( + keyPath: WritableKeyPath, + bindingAction: Self + ) -> Bool { + keyPath == bindingAction.keyPath + } + } + + extension Reducer { + @available( + *, deprecated, + message: + "'Reducer.binding()' no longer takes an explicit extract function and instead the reducer's 'Action' type must conform to 'BindableAction'. Upgrade to Xcode 12.5 or greater for access to 'Reducer.binding()' and 'BindableAction'." + ) + public func binding(action toBindingAction: @escaping (Action) -> BindingAction?) + -> Self + { + Self { state, action, environment in + toBindingAction(action)?.set(&state) + return self.run(&state, action, environment) + } + } + } + + extension ViewStore { + @available( + *, deprecated, + message: + "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState'. Bindings are now derived via dynamic member lookup to that 'BindableState' (for example, 'viewStore.$value'). For dynamic member lookup to be available, the view store's 'Action' type must also conform to 'BindableAction'. Upgrade to Xcode 12.5 or greater for access to 'BindableState' and 'BindableAction'." + ) + public func binding( + keyPath: WritableKeyPath, + send action: @escaping (BindingAction) -> Action + ) -> Binding + where LocalState: Equatable { + self.binding( + get: { $0[keyPath: keyPath] }, + send: { action(.set(keyPath, $0)) } + ) + } + } + #endif + + // NB: Deprecated after 0.23.0: + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.Button { + @available(*, deprecated, renamed: "cancel(_:action:)") + public static func cancel( + _ label: TextState, + send action: Action? + ) -> Self { + Self(action: action.map(AlertState.ButtonAction.send), type: .cancel(label: label)) + } + + @available(*, deprecated, renamed: "cancel(action:)") + public static func cancel( + send action: Action? + ) -> Self { + Self(action: action.map(AlertState.ButtonAction.send), type: .cancel(label: nil)) + } + + @available(*, deprecated, renamed: "default(_:action:)") + public static func `default`( + _ label: TextState, + send action: Action? + ) -> Self { + Self(action: action.map(AlertState.ButtonAction.send), type: .default(label: label)) + } + + @available(*, deprecated, renamed: "destructive(_:action:)") + public static func destructive( + _ label: TextState, + send action: Action? + ) -> Self { + Self(action: action.map(AlertState.ButtonAction.send), type: .destructive(label: label)) + } + } + + // NB: Deprecated after 0.20.0: + + extension Reducer { + @available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead") + public func forEach( + state toLocalState: WritableKeyPath, + action toLocalAction: CasePath, + environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, + breakpointOnNil: Bool = true, + file: StaticString = #fileID, + line: UInt = #line + ) -> Reducer { + .init { globalState, globalAction, globalEnvironment in + guard let (index, localAction) = toLocalAction.extract(from: globalAction) else { + return .none + } + if index >= globalState[keyPath: toLocalState].endIndex { + if breakpointOnNil { + breakpoint( + """ + --- + Warning: Reducer.forEach@\(file):\(line) + + "\(debugCaseOutput(localAction))" was received by a "forEach" reducer at index \ + \(index) when its state contained no element at this index. This is generally \ + considered an application logic error, and can happen for a few reasons: + + * This "forEach" reducer was combined with or run from another reducer that removed \ + the element at this index when it handled this action. To fix this make sure that \ + this "forEach" reducer is run before any other reducers that can move or remove \ + elements from state. This ensures that "forEach" reducers can handle their actions \ + for the element at the intended index. + + * An in-flight effect emitted this action while state contained no element at this \ + index. While it may be perfectly reasonable to ignore this action, you may want to \ + cancel the associated effect when moving or removing an element. If your "forEach" \ + reducer returns any long-living effects, you should use the identifier-based \ + "forEach" instead. + + * This action was sent to the store while its state contained no element at this \ + index. To fix this make sure that actions for this reducer can only be sent to a \ + view store when its state contains an element at this index. In SwiftUI \ + applications, use "ForEachStore". + --- + """ + ) + } + return .none + } + return self.run( + &globalState[keyPath: toLocalState][index], + localAction, + toLocalEnvironment(globalEnvironment) + ) + .map { toLocalAction.embed((index, $0)) } + } + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ForEachStore { + @available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead") + public init( + _ store: Store, + id: KeyPath, + @ViewBuilder content: @escaping (Store) -> EachContent + ) + where + Data == [EachState], + EachContent: View, + Content == WithViewStore< + [ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent> + > + { + let data = store.state.value + self.data = data + self.content = { + WithViewStore(store.scope(state: { $0.map { $0[keyPath: id] } })) { viewStore in + ForEach(Array(viewStore.state.enumerated()), id: \.element) { index, _ in + content( + store.scope( + state: { index < $0.endIndex ? $0[index] : data[index] }, + action: { (index, $0) } + ) + ) + } + } + } + } + + @available(*, deprecated, message: "Use the 'IdentifiedArray'-based version, instead") + public init( + _ store: Store, + @ViewBuilder content: @escaping (Store) -> EachContent + ) + where + Data == [EachState], + EachContent: View, + Content == WithViewStore< + [ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent> + >, + EachState: Identifiable, + EachState.ID == ID + { + self.init(store, id: \.id, content: content) + } + } + + // NB: Deprecated after 0.17.0: + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension IfLetStore { + @available(*, deprecated, message: "'else' now takes a view builder closure") + public init( + _ store: Store, + @ViewBuilder then ifContent: @escaping (Store) -> IfContent, + else elseContent: @escaping @autoclosure () -> ElseContent + ) where Content == _ConditionalContent { + self.init(store, then: ifContent, else: elseContent) + } + } + + // NB: Deprecated after 0.10.0: + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ActionSheetState { + @available(*, deprecated, message: "'title' and 'message' should be 'TextState'") + @_disfavoredOverload + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil, + buttons: [Button] + ) { + self.init( + title: .init(title), + message: message.map { .init($0) }, + buttons: buttons + ) + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension AlertState { + @available(*, deprecated, message: "'title' and 'message' should be 'TextState'") + @_disfavoredOverload + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil, + dismissButton: Button? = nil + ) { + self.init( + title: .init(title), + message: message.map { .init($0) }, + dismissButton: dismissButton + ) + } + + @available(*, deprecated, message: "'title' and 'message' should be 'TextState'") + @_disfavoredOverload + public init( + title: LocalizedStringKey, + message: LocalizedStringKey? = nil, + primaryButton: Button, + secondaryButton: Button + ) { + self.init( + title: .init(title), + message: message.map { .init($0) }, + primaryButton: primaryButton, + secondaryButton: secondaryButton + ) + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension AlertState.Button { + @available(*, deprecated, message: "'label' should be 'TextState'") + @_disfavoredOverload + public static func cancel( + _ label: LocalizedStringKey, + send action: Action? = nil + ) -> Self { + Self(action: action.map(AlertState.ButtonAction.send), type: .cancel(label: .init(label))) + } + + @available(*, deprecated, message: "'label' should be 'TextState'") + @_disfavoredOverload + public static func `default`( + _ label: LocalizedStringKey, + send action: Action? = nil + ) -> Self { + Self(action: action.map(AlertState.ButtonAction.send), type: .default(label: .init(label))) + } + + @available(*, deprecated, message: "'label' should be 'TextState'") + @_disfavoredOverload + public static func destructive( + _ label: LocalizedStringKey, + send action: Action? = nil + ) -> Self { + Self( + action: action.map(AlertState.ButtonAction.send), type: .destructive(label: .init(label))) + } + } +#endif + +// NB: Deprecated after 0.9.0: + +//extension Store { +// @_disfavoredOverload +// @available(*, deprecated, renamed: "producerScope(state:)") +// public func scope( +// state toLocalState: @escaping (Effect) -> Effect +// ) -> Effect, Never> { +// self.producerScope(state: toLocalState) +// } +// +// @_disfavoredOverload +// @available(*, deprecated, renamed: "producerScope(state:action:)") +// public func scope( +// state toLocalState: @escaping (Effect) -> Effect, +// action fromLocalAction: @escaping (LocalAction) -> Action +// ) -> Effect, Never> { +// self.producerScope(state: toLocalState, action: fromLocalAction) +// } +//} +// +//// NB: Deprecated after 0.6.0: +// +//extension Reducer { +// @available(*, deprecated, renamed: "optional()") +// public var optional: Reducer { +// self.optional() +// } +//} diff --git a/Sources/ComposableArchitecture/Internal/Diff.swift b/Sources/ComposableArchitecture/Internal/Diff.swift new file mode 100644 index 0000000..a3e47b3 --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Diff.swift @@ -0,0 +1,82 @@ +func diff(_ first: String, _ second: String) -> String? { + struct Difference { + enum Which { + case both + case first + case second + + var prefix: StaticString { + switch self { + case .both: return "\u{2007}" + case .first: return "−" + case .second: return "+" + } + } + } + + let elements: ArraySlice + let which: Which + } + + func diffHelp(_ first: ArraySlice, _ second: ArraySlice) -> [Difference] { + var indicesForLine: [Substring: [Int]] = [:] + for (firstIndex, firstLine) in zip(first.indices, first) { + indicesForLine[firstLine, default: []].append(firstIndex) + } + + var overlap: [Int: Int] = [:] + var firstIndex = first.startIndex + var secondIndex = second.startIndex + var count = 0 + + for (index, secondLine) in zip(second.indices, second) { + var innerOverlap: [Int: Int] = [:] + var innerFirstIndex = firstIndex + var innerSecondIndex = secondIndex + var innerCount = count + + indicesForLine[secondLine]?.forEach { firstIndex in + let newCount = (overlap[firstIndex - 1] ?? 0) + 1 + innerOverlap[firstIndex] = newCount + if newCount > count { + innerFirstIndex = firstIndex - newCount + 1 + innerSecondIndex = index - newCount + 1 + innerCount = newCount + } + } + + overlap = innerOverlap + firstIndex = innerFirstIndex + secondIndex = innerSecondIndex + count = innerCount + } + + if count == 0 { + var differences: [Difference] = [] + if !first.isEmpty { differences.append(Difference(elements: first, which: .first)) } + if !second.isEmpty { differences.append(Difference(elements: second, which: .second)) } + return differences + } else { + var differences = diffHelp(first.prefix(upTo: firstIndex), second.prefix(upTo: secondIndex)) + differences.append( + Difference(elements: first.suffix(from: firstIndex).prefix(count), which: .both)) + differences.append( + contentsOf: diffHelp( + first.suffix(from: firstIndex + count), second.suffix(from: secondIndex + count))) + return differences + } + } + + let differences = diffHelp( + first.split(separator: "\n", omittingEmptySubsequences: false)[...], + second.split(separator: "\n", omittingEmptySubsequences: false)[...] + ) + if differences.count == 1, case .both = differences[0].which { return nil } + var string = differences.reduce(into: "") { string, diff in + diff.elements.forEach { line in + string += "\(diff.which.prefix) \(line)\n" + } + } + string.removeLast() + return string +} diff --git a/Sources/ComposableArchitecture/Internal/Exports.swift b/Sources/ComposableArchitecture/Internal/Exports.swift new file mode 100644 index 0000000..6aa0046 --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Exports.swift @@ -0,0 +1,5 @@ +@_exported import CasePaths +@_exported import CombineSchedulers +@_exported import CustomDump +@_exported import IdentifiedCollections +@_exported import ReactiveSwift diff --git a/Sources/ComposableArchitecture/Internal/Locking.swift b/Sources/ComposableArchitecture/Internal/Locking.swift new file mode 100644 index 0000000..76c316e --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Locking.swift @@ -0,0 +1,21 @@ +import Foundation + +#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS) + extension UnsafeMutablePointer where Pointee == os_unfair_lock_s { + @inlinable @discardableResult + func sync(_ work: () -> R) -> R { + os_unfair_lock_lock(self) + defer { os_unfair_lock_unlock(self) } + return work() + } + } +#endif + +extension NSRecursiveLock { + @inlinable @discardableResult + func sync(work: () -> R) -> R { + self.lock() + defer { self.unlock() } + return work() + } +} diff --git a/Sources/ComposableArchitecture/Reducer.swift b/Sources/ComposableArchitecture/Reducer.swift new file mode 100644 index 0000000..8b20392 --- /dev/null +++ b/Sources/ComposableArchitecture/Reducer.swift @@ -0,0 +1,253 @@ +import CasePaths + +public struct Reducer { + private let reducer: (inout State, Action, Environment) -> Effect + + public init(_ reducer: @escaping (inout State, Action, Environment) -> Effect) { + self.reducer = reducer + } + + public static var empty: Reducer { + Self { _, _, _ in .none } + } + + public static func combine(_ reducers: Reducer...) -> Reducer { + .combine(reducers) + } + + public static func combine(_ reducers: [Reducer]) -> Reducer { + Self { value, action, environment in + .merge(reducers.map { $0.reducer(&value, action, environment) }) + } + } + + public func combined(with other: Reducer) -> Reducer { + .combine(self, other) + } + + public func pullback( + state toLocalState: WritableKeyPath, + action toLocalAction: CasePath, + environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment + ) -> Reducer { + .init { globalState, globalAction, globalEnvironment in + guard let localAction = toLocalAction.extract(from: globalAction) else { return .none } + return self.reducer( + &globalState[keyPath: toLocalState], + localAction, + toLocalEnvironment(globalEnvironment) + ) + .map(toLocalAction.embed) + } + } + + public func pullback( + state toLocalState: CasePath, + action toLocalAction: CasePath, + environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, + breakpointOnNil: Bool = true, + file: StaticString = #fileID, + line: UInt = #line + ) -> Reducer { + .init { globalState, globalAction, globalEnvironment in + guard let localAction = toLocalAction.extract(from: globalAction) else { return .none } + + guard var localState = toLocalState.extract(from: globalState) else { + if breakpointOnNil { + breakpoint( + """ + --- + Warning: Reducer.pullback@\(file):\(line) + + "\(debugCaseOutput(localAction))" was received by a reducer when its state was \ + unavailable. This is generally considered an application logic error, and can happen \ + for a few reasons: + + * The reducer for a particular case of state was combined with or run from another \ + reducer that set "\(State.self)" to another case before the reducer ran. Combine or \ + run case-specific reducers before reducers that may set their state to another case. \ + This ensures that case-specific reducers can handle their actions while their state \ + is available. + + * An in-flight effect emitted this action when state was unavailable. While it may be \ + perfectly reasonable to ignore this action, you may want to cancel the associated \ + effect before state is set to another case, especially if it is a long-living effect. + + * This action was sent to the store while state was another case. Make sure that \ + actions for this reducer can only be sent to a view store when state is non-"nil". \ + In SwiftUI applications, use "SwitchStore". + --- + """ + ) + } + return .none + } + defer { globalState = toLocalState.embed(localState) } + let effects = self.run( + &localState, + localAction, + toLocalEnvironment(globalEnvironment) + ) + .map(toLocalAction.embed) + return effects + } + } + + public func optional( + breakpointOnNil: Bool = true, + file: StaticString = #fileID, + line: UInt = #line + ) -> Reducer< + State?, Action, Environment + > { + .init { state, action, environment in + guard state != nil else { + if breakpointOnNil { + breakpoint( + """ + --- + Warning: Reducer.optional@\(file):\(line) + + "\(debugCaseOutput(action))" was received by an optional reducer when its state was \ + "nil". This is generally considered an application logic error, and can happen for a \ + few reasons: + + * The optional reducer was combined with or run from another reducer that set \ + "\(State.self)" to "nil" before the optional reducer ran. Combine or run optional \ + reducers before reducers that can set their state to "nil". This ensures that optional \ + reducers can handle their actions while their state is still non-"nil". + + * An in-flight effect emitted this action while state was "nil". While it may be \ + perfectly reasonable to ignore this action, you may want to cancel the associated \ + effect before state is set to "nil", especially if it is a long-living effect. + + * This action was sent to the store while state was "nil". Make sure that actions for \ + this reducer can only be sent to a view store when state is non-"nil". In SwiftUI \ + applications, use "IfLetStore". + --- + """ + ) + } + return .none + } + return self.reducer(&state!, action, environment) + } + } + + public func forEach( + state toLocalState: WritableKeyPath>, + action toLocalAction: CasePath, + environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, + breakpointOnNil: Bool = true, + file: StaticString = #fileID, + line: UInt = #line + ) -> Reducer { + .init { globalState, globalAction, globalEnvironment in + guard let (id, localAction) = toLocalAction.extract(from: globalAction) else { return .none } + if globalState[keyPath: toLocalState][id: id] == nil { + if breakpointOnNil { + breakpoint( + """ + --- + Warning: Reducer.forEach@\(file):\(line) + + "\(debugCaseOutput(localAction))" was received by a "forEach" reducer at id \(id) when \ + its state contained no element at this id. This is generally considered an application \ + logic error, and can happen for a few reasons: + + * This "forEach" reducer was combined with or run from another reducer that removed \ + the element at this id when it handled this action. To fix this make sure that this \ + "forEach" reducer is run before any other reducers that can move or remove elements \ + from state. This ensures that "forEach" reducers can handle their actions for the \ + element at the intended id. + + * An in-flight effect emitted this action while state contained no element at this id. \ + It may be perfectly reasonable to ignore this action, but you also may want to cancel \ + the effect it originated from when removing an element from the identified array, \ + especially if it is a long-living effect. + + * This action was sent to the store while its state contained no element at this id. \ + To fix this make sure that actions for this reducer can only be sent to a view store \ + when its state contains an element at this id. In SwiftUI applications, use \ + "ForEachStore". + --- + """ + ) + } + return .none + } + return self.reducer( + &globalState[keyPath: toLocalState][id: id]!, + localAction, + toLocalEnvironment(globalEnvironment) + ) + .map { toLocalAction.embed((id, $0)) } + } + } + + public func forEach( + state toLocalState: WritableKeyPath, + action toLocalAction: CasePath, + environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, + breakpointOnNil: Bool = true, + file: StaticString = #fileID, + line: UInt = #line + ) -> Reducer { + .init { globalState, globalAction, globalEnvironment in + guard let (key, localAction) = toLocalAction.extract(from: globalAction) else { return .none } + if globalState[keyPath: toLocalState][key] == nil { + if breakpointOnNil { + breakpoint( + """ + --- + Warning: Reducer.forEach@\(file):\(line) + + "\(debugCaseOutput(localAction))" was received by a "forEach" reducer at key \(key) \ + when its state contained no element at this key. This is generally considered an \ + application logic error, and can happen for a few reasons: + + * This "forEach" reducer was combined with or run from another reducer that removed \ + the element at this key when it handled this action. To fix this make sure that this \ + "forEach" reducer is run before any other reducers that can move or remove elements \ + from state. This ensures that "forEach" reducers can handle their actions for the \ + element at the intended key. + + * An in-flight effect emitted this action while state contained no element at this \ + key. It may be perfectly reasonable to ignore this action, but you also may want to \ + cancel the effect it originated from when removing a value from the dictionary, \ + especially if it is a long-living effect. + + * This action was sent to the store while its state contained no element at this \ + key. To fix this make sure that actions for this reducer can only be sent to a view \ + store when its state contains an element at this key. + --- + """ + ) + } + return .none + } + return self.reducer( + &globalState[keyPath: toLocalState][key]!, + localAction, + toLocalEnvironment(globalEnvironment) + ) + .map { toLocalAction.embed((key, $0)) } + } + } + + public func run( + _ state: inout State, + _ action: Action, + _ environment: Environment + ) -> Effect { + self.reducer(&state, action, environment) + } + + public func callAsFunction( + _ state: inout State, + _ action: Action, + _ environment: Environment + ) -> Effect { + self.reducer(&state, action, environment) + } +} diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift new file mode 100644 index 0000000..ceeafcd --- /dev/null +++ b/Sources/ComposableArchitecture/Store.swift @@ -0,0 +1,210 @@ +import Foundation +import ReactiveSwift + +public final class Store { + private var bufferedActions: [Action] = [] + var effectDisposables: [UUID: Disposable] = [:] + private var isSending = false + var parentDisposable: Disposable? + private let reducer: (inout State, Action) -> Effect + var state: MutableProperty + #if DEBUG + private let mainThreadChecksEnabled: Bool + #endif + + public convenience init( + initialState: State, + reducer: Reducer, + environment: Environment + ) { + self.init( + initialState: initialState, + reducer: reducer, + environment: environment, + mainThreadChecksEnabled: true + ) + self.threadCheck(status: .`init`) + } + + public static func unchecked( + initialState: State, + reducer: Reducer, + environment: Environment + ) -> Self { + Self( + initialState: initialState, + reducer: reducer, + environment: environment, + mainThreadChecksEnabled: false + ) + } + + public func scope( + state toLocalState: @escaping (State) -> LocalState, + action fromLocalAction: @escaping (LocalAction) -> Action + ) -> Store { + var isSending = false + let localStore = Store( + initialState: toLocalState(self.state.value), + reducer: .init { localState, localAction, _ in + isSending = true + defer { isSending = false } + self.send(fromLocalAction(localAction)) + localState = toLocalState(self.state.value) + return .none + }, + environment: () + ) + + localStore.parentDisposable = self.state.producer + .skip(first: 1) + .startWithValues { [weak localStore] newValue in + guard !isSending else { return } + localStore?.state.value = toLocalState(newValue) + } + + return localStore + } + + public func scope( + state toLocalState: @escaping (State) -> LocalState + ) -> Store { + self.scope(state: toLocalState, action: { $0 }) + } + + func send(_ action: Action, originatingFrom originatingAction: Action? = nil) { + self.threadCheck(status: .send(action, originatingAction: originatingAction)) + self.bufferedActions.append(action) + guard !self.isSending else { return } + self.isSending = true + var currentState = self.state.value + defer { + self.isSending = false + self.state.value = currentState + } + while !self.bufferedActions.isEmpty { + let action = self.bufferedActions.removeFirst() + let effect = self.reducer(¤tState, action) + var didComplete = false + let uuid = UUID() + let observer = Signal.Observer( + value: { [weak self] effectAction in + self?.send(effectAction, originatingFrom: action) + }, + completed: { [weak self] in + self?.threadCheck(status: .effectCompletion(action)) + didComplete = true + self?.effectDisposables.removeValue(forKey: uuid)?.dispose() + }, + interrupted: { [weak self] in + didComplete = true + self?.effectDisposables.removeValue(forKey: uuid)?.dispose() + } + ) + let effectDisposable = effect.start(observer) + if !didComplete { + self.effectDisposables[uuid] = effectDisposable + } + } + } + + public var stateless: Store { + self.scope(state: { _ in () }) + } + + public var actionless: Store { + func absurd(_ never: Never) -> A {} + return self.scope(state: { $0 }, action: absurd) + } + + deinit { + self.parentDisposable?.dispose() + self.effectDisposables.keys.forEach { id in + self.effectDisposables.removeValue(forKey: id)?.dispose() + } + } + private enum ThreadCheckStatus { + case effectCompletion(Action) + case `init` + case scope + case send(Action, originatingAction: Action?) + } + + @inline(__always) + private func threadCheck(status: ThreadCheckStatus) { + #if DEBUG + guard self.mainThreadChecksEnabled && !Thread.isMainThread + else { return } + + let message: String + switch status { + case let .effectCompletion(action): + message = """ + An effect returned from the action "\(debugCaseOutput(action))" completed on a non-main \ + thread. Make sure to use ".receive(on:)" on any effects that execute on background \ + threads to receive their output on the main thread, or create this store via \ + "Store.unchecked" to disable the main thread checker. + """ + + case .`init`: + message = """ + "Store.init" was called on a non-main thread. Make sure that stores are initialized on \ + the main thread, or create this store via "Store.unchecked" to disable the main thread \ + checker. + """ + + case .scope: + message = """ + "Store.scope" was called on a non-main thread. Make sure that "Store.scope" is always \ + called on the main thread, or create this store via "Store.unchecked" to disable the \ + main thread checker. + """ + + case let .send(action, originatingAction: nil): + message = """ + "ViewStore.send(\(debugCaseOutput(action)))" was called on a non-main thread. Make sure \ + that "ViewStore.send" is always called on the main thread, or create this store via \ + "Store.unchecked" to disable the main thread checker. + """ + + case let .send(action, originatingAction: .some(originatingAction)): + message = """ + An effect returned from "\(debugCaseOutput(originatingAction))" emitted the action \ + "\(debugCaseOutput(action))" on a non-main thread. Make sure to use ".receive(on:)" on \ + any effects that execute on background threads to receive their output on the main \ + thread, or create this store via "Store.unchecked" to disable the main thread checker. + """ + } + + breakpoint( + """ + --- + Warning: + + A store created on the main thread was interacted with on a non-main thread: + + Thread: \(Thread.current) + + \(message) + + The "Store" class is not thread-safe, and so all interactions with an instance of "Store" \ + (including all of its scopes and derived view stores) must be done on the main thread. + --- + """ + ) + #endif + } + + private init( + initialState: State, + reducer: Reducer, + environment: Environment, + mainThreadChecksEnabled: Bool + ) { + self.state = MutableProperty(initialState) + self.reducer = { state, action in reducer.run(&state, action, environment) } + #if DEBUG + self.mainThreadChecksEnabled = mainThreadChecksEnabled + #endif + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift new file mode 100644 index 0000000..23660b3 --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift @@ -0,0 +1,96 @@ +#if canImport(SwiftUI) + import SwiftUI + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + public struct ActionSheetState { + public let id = UUID() + public var buttons: [Button] + public var message: TextState? + public var title: TextState + + public init( + title: TextState, + message: TextState? = nil, + buttons: [Button] + ) { + self.buttons = buttons + self.message = message + self.title = title + } + + public typealias Button = AlertState.Button + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ActionSheetState: CustomDebugOutputConvertible { + public var debugOutput: String { + let fields = ( + title: self.title, + message: self.message, + buttons: self.buttons + ) + return "\(Self.self)\(ComposableArchitecture.debugOutput(fields))" + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ActionSheetState: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.message + && lhs.buttons == rhs.buttons + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ActionSheetState: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.title) + hasher.combine(self.message) + hasher.combine(self.buttons) + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ActionSheetState: Identifiable {} + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension View { + /// Displays an action sheet when the store's state becomes non-`nil`, and dismisses it when it + /// becomes `nil`. + /// + /// - Parameters: + /// - store: A store that describes if the action sheet is shown or dismissed. + /// - dismissal: An action to send when the action sheet is dismissed through non-user actions, + /// such as when an action sheet is automatically dismissed by the system. Use this action to + /// `nil` out the associated action sheet state. + @available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *) + @available(macOS, unavailable) + public func actionSheet( + _ store: Store?, Action>, + dismiss: Action + ) -> some View { + + WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in + self.actionSheet(item: viewStore.binding(send: dismiss)) { state in + state.toSwiftUI(send: viewStore.send) + } + } + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension ActionSheetState { + @available(iOS 13, macCatalyst 13, tvOS 13, watchOS 6, *) + @available(macOS, unavailable) + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet { + SwiftUI.ActionSheet( + title: Text(self.title), + message: self.message.map { Text($0) }, + buttons: self.buttons.map { + $0.toSwiftUI(send: send) + } + ) + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionWrappingScheduler.swift b/Sources/ComposableArchitecture/SwiftUI/ActionWrappingScheduler.swift new file mode 100644 index 0000000..d68236a --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/ActionWrappingScheduler.swift @@ -0,0 +1,107 @@ +#if canImport(SwiftUI) + import SwiftUI + import ReactiveSwift + + extension Scheduler { + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func animation(_ animation: Animation? = .default) -> Scheduler { + ActionWrappingScheduler(scheduler: self, wrapper: .animation(animation)) + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func transaction(_ transaction: Transaction) -> Scheduler { + ActionWrappingScheduler(scheduler: self, wrapper: .transaction(transaction)) + } + } + + extension DateScheduler { + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func animation(_ animation: Animation? = .default) -> DateScheduler { + ActionWrappingDateScheduler(scheduler: self, wrapper: .animation(animation)) + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func transaction(_ transaction: Transaction) -> DateScheduler { + ActionWrappingDateScheduler(scheduler: self, wrapper: .transaction(transaction)) + } + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + private enum ActionWrapper { + case animation(Animation?) + case transaction(Transaction) + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public final class ActionWrappingScheduler: Scheduler { + private let scheduler: Scheduler + private let wrapper: ActionWrapper + + fileprivate init(scheduler: Scheduler, wrapper: ActionWrapper) { + self.scheduler = scheduler + self.wrapper = wrapper + } + + public func schedule(_ action: @escaping () -> Void) -> Disposable? { + scheduler.schedule { + switch self.wrapper { + case let .animation(animation): + withAnimation(animation, action) + case let .transaction(transaction): + withTransaction(transaction, action) + } + } + } + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public final class ActionWrappingDateScheduler: DateScheduler { + public var currentDate: Date { + scheduler.currentDate + } + + private let scheduler: DateScheduler + private let wrapper: ActionWrapper + + fileprivate init(scheduler: DateScheduler, wrapper: ActionWrapper) { + self.scheduler = scheduler + self.wrapper = wrapper + } + + public func schedule(_ action: @escaping () -> Void) -> Disposable? { + scheduler.schedule { + switch self.wrapper { + case let .animation(animation): + withAnimation(animation, action) + case let .transaction(transaction): + withTransaction(transaction, action) + } + } + } + + public func schedule(after date: Date, action: @escaping () -> Void) -> Disposable? { + scheduler.schedule(after: date) { + switch self.wrapper { + case let .animation(animation): + withAnimation(animation, action) + case let .transaction(transaction): + withTransaction(transaction, action) + } + } + } + + public func schedule( + after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, + action: @escaping () -> Void + ) -> Disposable? { + scheduler.schedule(after: date, interval: interval, leeway: leeway) { + switch self.wrapper { + case let .animation(animation): + withAnimation(animation, action) + case let .transaction(transaction): + withTransaction(transaction, action) + } + } + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/Alert.swift b/Sources/ComposableArchitecture/SwiftUI/Alert.swift new file mode 100644 index 0000000..7424943 --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -0,0 +1,225 @@ +#if canImport(SwiftUI) + import SwiftUI + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + public struct AlertState { + public let id = UUID() + public var message: TextState? + public var primaryButton: Button? + public var secondaryButton: Button? + public var title: TextState + + public init( + title: TextState, + message: TextState? = nil, + dismissButton: Button? = nil + ) { + self.title = title + self.message = message + self.primaryButton = dismissButton + } + + public init( + title: TextState, + message: TextState? = nil, + primaryButton: Button, + secondaryButton: Button + ) { + self.title = title + self.message = message + self.primaryButton = primaryButton + self.secondaryButton = secondaryButton + } + + public struct Button { + public var action: ButtonAction? + public var type: ButtonType + + public static func cancel( + _ label: TextState, + action: ButtonAction? = nil + ) -> Self { + Self(action: action, type: .cancel(label: label)) + } + + public static func cancel( + action: ButtonAction? = nil + ) -> Self { + Self(action: action, type: .cancel(label: nil)) + } + + public static func `default`( + _ label: TextState, + action: ButtonAction? = nil + ) -> Self { + Self(action: action, type: .default(label: label)) + } + + public static func destructive( + _ label: TextState, + action: ButtonAction? = nil + ) -> Self { + Self(action: action, type: .destructive(label: label)) + } + } + + public struct ButtonAction { + let type: ActionType + + public static func send(_ action: Action) -> Self { + .init(type: .send(action)) + } + + public static func send(_ action: Action, animation: Animation?) -> Self { + .init(type: .animatedSend(action, animation: animation)) + } + + enum ActionType { + case send(Action) + case animatedSend(Action, animation: Animation?) + } + } + + public enum ButtonType { + case cancel(label: TextState?) + case `default`(label: TextState) + case destructive(label: TextState) + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension View { + /// Displays an alert when then store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that describes if the alert is shown or dismissed. + /// - dismissal: An action to send when the alert is dismissed through non-user actions, such + /// as when an alert is automatically dismissed by the system. Use this action to `nil` out + /// the associated alert state. + public func alert( + _ store: Store?, Action>, + dismiss: Action + ) -> some View { + + WithViewStore(store, removeDuplicates: { $0?.id == $1?.id }) { viewStore in + self.alert(item: viewStore.binding(send: dismiss)) { state in + state.toSwiftUI(send: viewStore.send) + } + } + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState: CustomDebugOutputConvertible { + public var debugOutput: String { + let fields = ( + title: self.title, + message: self.message, + primaryButton: self.primaryButton, + secondaryButton: self.secondaryButton + ) + return "\(Self.self)\(ComposableArchitecture.debugOutput(fields))" + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.message + && lhs.primaryButton == rhs.primaryButton + && lhs.secondaryButton == rhs.secondaryButton + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.title) + hasher.combine(self.message) + hasher.combine(self.primaryButton) + hasher.combine(self.secondaryButton) + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState: Identifiable {} + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.ButtonAction: Equatable where Action: Equatable {} + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.ButtonAction.ActionType: Equatable where Action: Equatable {} + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.ButtonType: Equatable {} + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.Button: Equatable where Action: Equatable {} + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.ButtonAction: Hashable where Action: Hashable {} + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.ButtonAction.ActionType: Hashable where Action: Hashable { + func hash(into hasher: inout Hasher) { + switch self { + case let .send(action), let .animatedSend(action, animation: _): + hasher.combine(action) + } + } + } + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.ButtonType: Hashable {} + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.Button: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.action) + hasher.combine(self.type) + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState.Button { + func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + let action = { + switch self.action?.type { + case .none: + return + case let .some(.send(action)): + send(action) + case let .some(.animatedSend(action, animation: animation)): + withAnimation(animation) { send(action) } + } + } + switch self.type { + case let .cancel(.some(label)): + return .cancel(Text(label), action: action) + case .cancel(.none): + return .cancel(action) + case let .default(label): + return .default(Text(label), action: action) + case let .destructive(label): + return .destructive(Text(label), action: action) + } + } + } + + @available(iOS 13.0, macOS 10.15, macCatalyst 13, tvOS 13.0, watchOS 6.0, *) + extension AlertState { + fileprivate func toSwiftUI(send: @escaping (Action) -> Void) -> SwiftUI.Alert { + if let primaryButton = self.primaryButton, let secondaryButton = self.secondaryButton { + return SwiftUI.Alert( + title: Text(self.title), + message: self.message.map { Text($0) }, + primaryButton: primaryButton.toSwiftUI(send: send), + secondaryButton: secondaryButton.toSwiftUI(send: send) + ) + } else { + return SwiftUI.Alert( + title: Text(self.title), + message: self.message.map { Text($0) }, + dismissButton: self.primaryButton?.toSwiftUI(send: send) + ) + } + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/Animation.swift b/Sources/ComposableArchitecture/SwiftUI/Animation.swift new file mode 100644 index 0000000..d9349ef --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/Animation.swift @@ -0,0 +1,12 @@ +#if canImport(SwiftUI) + import SwiftUI + + extension ViewStore { + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func send(_ action: Action, animation: Animation?) { + withAnimation(animation) { + self.send(action) + } + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/Binding.swift b/Sources/ComposableArchitecture/SwiftUI/Binding.swift new file mode 100644 index 0000000..04ad13a --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -0,0 +1,178 @@ +import CustomDump +import SwiftUI + +#if compiler(>=5.4) + @dynamicMemberLookup + @propertyWrapper + public struct BindableState { + public var wrappedValue: Value + public init(wrappedValue: Value) { + self.wrappedValue = wrappedValue + } + + public var projectedValue: Self { + get { self } + set { self = newValue } + } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> BindableState { + get { .init(wrappedValue: self.wrappedValue[keyPath: keyPath]) } + set { self.wrappedValue[keyPath: keyPath] = newValue.wrappedValue } + } + } + + extension BindableState: Equatable where Value: Equatable {} + + extension BindableState: Hashable where Value: Hashable {} + + extension BindableState: Decodable where Value: Decodable { + public init(from decoder: Decoder) throws { + do { + let container = try decoder.singleValueContainer() + self.init(wrappedValue: try container.decode(Value.self)) + } catch { + self.init(wrappedValue: try Value(from: decoder)) + } + } + } + + extension BindableState: Encodable where Value: Encodable { + public func encode(to encoder: Encoder) throws { + do { + var container = encoder.singleValueContainer() + try container.encode(self.wrappedValue) + } catch { + try self.wrappedValue.encode(to: encoder) + } + } + } + + extension BindableState: CustomReflectable { + public var customMirror: Mirror { + Mirror(reflecting: self.wrappedValue) + } + } + + extension BindableState: CustomDumpRepresentable { + public var customDumpValue: Any { + self.wrappedValue + } + } + + extension BindableState: CustomDebugStringConvertible where Value: CustomDebugStringConvertible { + public var debugDescription: String { + self.wrappedValue.debugDescription + } + } + + public protocol BindableAction { + + associatedtype State + + static func binding(_ action: BindingAction) -> Self + } + + extension BindableAction { + + public static func set( + _ keyPath: WritableKeyPath>, + _ value: Value + ) -> Self + where Value: Equatable { + self.binding(.set(keyPath, value)) + } + } + + extension ViewStore { + + public func binding( + _ keyPath: WritableKeyPath> + ) -> Binding + where Action: BindableAction, Action.State == State, Value: Equatable { + self.binding( + get: { $0[keyPath: keyPath].wrappedValue }, + send: { .binding(.set(keyPath, $0)) } + ) + } + } +#endif + +public struct BindingAction: Equatable { + public let keyPath: PartialKeyPath + + let set: (inout Root) -> Void + let value: Any + let valueIsEqualTo: (Any) -> Bool + + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.keyPath == rhs.keyPath && lhs.valueIsEqualTo(rhs.value) + } +} + +#if compiler(>=5.4) + extension BindingAction { + + public static func set( + _ keyPath: WritableKeyPath>, + _ value: Value + ) -> Self where Value: Equatable { + .init( + keyPath: keyPath, + set: { $0[keyPath: keyPath].wrappedValue = value }, + value: value, + valueIsEqualTo: { $0 as? Value == value } + ) + } + + public static func ~= ( + keyPath: WritableKeyPath>, + bindingAction: Self + ) -> Bool { + keyPath == bindingAction.keyPath + } + } +#endif + +extension BindingAction { + + public func pullback( + _ keyPath: WritableKeyPath + ) -> BindingAction { + .init( + keyPath: (keyPath as AnyKeyPath).appending(path: self.keyPath) as! PartialKeyPath, + set: { self.set(&$0[keyPath: keyPath]) }, + value: self.value, + valueIsEqualTo: self.valueIsEqualTo + ) + } +} + +extension BindingAction: CustomDumpReflectable { + public var customDumpMirror: Mirror { + .init( + self, + children: [ + "set": (self.keyPath, self.value) + ], + displayStyle: .enum + ) + } +} + +#if compiler(>=5.4) + extension Reducer where Action: BindableAction, State == Action.State { + public func binding() -> Self { + Self { state, action, environment in + guard let bindingAction = (/Action.binding).extract(from: action) + else { + return self.run(&state, action, environment) + } + + bindingAction.set(&state) + return self.run(&state, action, environment) + } + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift new file mode 100644 index 0000000..8b6fb5f --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift @@ -0,0 +1,42 @@ +import OrderedCollections +import SwiftUI + +public struct ForEachStore: DynamicViewContent +where Data: Collection, ID: Hashable, Content: View { + public let data: Data + let content: () -> Content + + public init( + _ store: Store, (ID, EachAction)>, + @ViewBuilder content: @escaping (Store) -> EachContent + ) + where + EachContent: View, + Data == IdentifiedArray, + Content == WithViewStore< + OrderedSet, (ID, EachAction), ForEach, ID, EachContent> + > + { + self.data = store.state.value + self.content = { + WithViewStore(store.scope(state: { $0.ids })) { viewStore in + ForEach(viewStore.state, id: \.self) { id -> EachContent in + var element = store.state.value[id: id]! + return content( + store.scope( + state: { + element = $0[id: id] ?? element + return element + }, + action: { (id, $0) } + ) + ) + } + } + } + } + + public var body: some View { + self.content() + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/Identified.swift b/Sources/ComposableArchitecture/SwiftUI/Identified.swift new file mode 100644 index 0000000..96034e7 --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/Identified.swift @@ -0,0 +1,33 @@ +@dynamicMemberLookup +public struct Identified: Identifiable where ID: Hashable { + public let id: ID + public var value: Value + + public init(_ value: Value, id: ID) { + self.id = id + self.value = value + } + + public init(_ value: Value, id: (Value) -> ID) { + self.init(value, id: id(value)) + } + + public init(_ value: Value, id: KeyPath) { + self.init(value, id: value[keyPath: id]) + } + + public subscript( + dynamicMember keyPath: WritableKeyPath + ) -> LocalValue { + get { self.value[keyPath: keyPath] } + set { self.value[keyPath: keyPath] = newValue } + } +} + +extension Identified: Decodable where ID: Decodable, Value: Decodable {} + +extension Identified: Encodable where ID: Encodable, Value: Encodable {} + +extension Identified: Equatable where Value: Equatable {} + +extension Identified: Hashable where Value: Hashable {} diff --git a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift new file mode 100644 index 0000000..897be22 --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift @@ -0,0 +1,57 @@ +#if canImport(SwiftUI) + import SwiftUI + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public struct IfLetStore: View where Content: View { + private let content: (ViewStore) -> Content + private let store: Store + + public init( + _ store: Store, + @ViewBuilder then ifContent: @escaping (Store) -> IfContent, + @ViewBuilder else elseContent: @escaping () -> ElseContent + ) where Content == _ConditionalContent { + self.store = store + self.content = { viewStore in + if var state = viewStore.state { + return ViewBuilder.buildEither( + first: ifContent( + store.scope { + state = $0 ?? state + return state + } + ) + ) + } else { + return ViewBuilder.buildEither(second: elseContent()) + } + } + } + + public init( + _ store: Store, + @ViewBuilder then ifContent: @escaping (Store) -> IfContent + ) where Content == IfContent? { + self.store = store + self.content = { viewStore in + if var state = viewStore.state { + return ifContent( + store.scope { + state = $0 ?? state + return state + } + ) + } else { + return nil + } + } + } + + public var body: some View { + WithViewStore( + self.store, + removeDuplicates: { ($0 != nil) == ($1 != nil) }, + content: self.content + ) + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift new file mode 100644 index 0000000..dd5f67b --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift @@ -0,0 +1,1163 @@ +#if canImport(SwiftUI) + import SwiftUI + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + public struct SwitchStore: View where Content: View { + public let store: Store + public let content: () -> Content + + init( + store: Store, + @ViewBuilder content: @escaping () -> Content + ) { + self.store = store + self.content = content + } + + public var body: some View { + self.content() + .environmentObject(StoreObservableObject(store: self.store)) + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + public struct CaseLet: View + where Content: View { + @EnvironmentObject private var store: StoreObservableObject + public let toLocalState: (GlobalState) -> LocalState? + public let fromLocalAction: (LocalAction) -> GlobalAction + public let content: (Store) -> Content + + public init( + state toLocalState: @escaping (GlobalState) -> LocalState?, + action fromLocalAction: @escaping (LocalAction) -> GlobalAction, + @ViewBuilder then content: @escaping (Store) -> Content + ) { + self.toLocalState = toLocalState + self.fromLocalAction = fromLocalAction + self.content = content + } + + public var body: some View { + IfLetStore( + self.store.wrappedValue.scope( + state: self.toLocalState, + action: self.fromLocalAction + ), + then: self.content + ) + } + } + + /// A view that covers any cases that aren't addressed in a ``SwitchStore``. + /// + /// If you wish to use ``SwitchStore`` in a non-exhaustive manner (i.e. you do not want to provide + /// a ``CaseLet`` for each case of the enum), then you must insert a ``Default`` view at the end of + /// the ``SwitchStore``'s body. + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + public struct Default: View where Content: View { + private let content: () -> Content + + /// Initializes a ``Default`` view that computes content depending on if a store of enum state + /// does not match a particular case. + /// + /// - Parameter content: A function that returns a view that is visible only when the switch + /// store's state does not match a preceding ``CaseLet`` view. + public init(@ViewBuilder content: @escaping () -> Content) { + self.content = content + } + + public var body: some View { + self.content() + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension SwitchStore { + public init( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + CaseLet, + Default + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else { + content.1 + } + } + } + } + + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> CaseLet + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + { + self.init(store) { + content() + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else { + content.2 + } + } + } + } + + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default + > + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else { + content.3 + } + } + } + } + + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + Default + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else if content.3.toLocalState(viewStore.state) != nil { + content.3 + } else { + content.4 + } + } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4 + >( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + CaseLet, + Default + > + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else if content.3.toLocalState(viewStore.state) != nil { + content.3 + } else if content.4.toLocalState(viewStore.state) != nil { + content.4 + } else { + content.5 + } + } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5 + >( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default + > + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else if content.3.toLocalState(viewStore.state) != nil { + content.3 + } else if content.4.toLocalState(viewStore.state) != nil { + content.4 + } else if content.5.toLocalState(viewStore.state) != nil { + content.5 + } else { + content.6 + } + } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6 + >( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + Default<_ExhaustivityCheckView> + > + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + State7, Action7, Content7, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default + > + > + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else if content.3.toLocalState(viewStore.state) != nil { + content.3 + } else if content.4.toLocalState(viewStore.state) != nil { + content.4 + } else if content.5.toLocalState(viewStore.state) != nil { + content.5 + } else if content.6.toLocalState(viewStore.state) != nil { + content.6 + } else { + content.7 + } + } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + State7, Action7, Content7 + >( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + State7, Action7, Content7, + State8, Action8, Content8, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + Default + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else if content.3.toLocalState(viewStore.state) != nil { + content.3 + } else if content.4.toLocalState(viewStore.state) != nil { + content.4 + } else if content.5.toLocalState(viewStore.state) != nil { + content.5 + } else if content.6.toLocalState(viewStore.state) != nil { + content.6 + } else if content.7.toLocalState(viewStore.state) != nil { + content.7 + } else { + content.8 + } + } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + State7, Action7, Content7, + State8, Action8, Content8 + >( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + Default<_ExhaustivityCheckView> + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + content.value.7 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + State7, Action7, Content7, + State8, Action8, Content8, + State9, Action9, Content9, + DefaultContent + >( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + _ConditionalContent< + CaseLet, + Default + > + > + > + { + self.init(store: store) { + let content = content().value + return WithViewStore(store, removeDuplicates: { enumTag($0) == enumTag($1) }) { viewStore in + if content.0.toLocalState(viewStore.state) != nil { + content.0 + } else if content.1.toLocalState(viewStore.state) != nil { + content.1 + } else if content.2.toLocalState(viewStore.state) != nil { + content.2 + } else if content.3.toLocalState(viewStore.state) != nil { + content.3 + } else if content.4.toLocalState(viewStore.state) != nil { + content.4 + } else if content.5.toLocalState(viewStore.state) != nil { + content.5 + } else if content.6.toLocalState(viewStore.state) != nil { + content.6 + } else if content.7.toLocalState(viewStore.state) != nil { + content.7 + } else if content.8.toLocalState(viewStore.state) != nil { + content.8 + } else { + content.9 + } + } + } + } + + public init< + State1, Action1, Content1, + State2, Action2, Content2, + State3, Action3, Content3, + State4, Action4, Content4, + State5, Action5, Content5, + State6, Action6, Content6, + State7, Action7, Content7, + State8, Action8, Content8, + State9, Action9, Content9 + >( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet, + CaseLet + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + >, + _ConditionalContent< + _ConditionalContent< + CaseLet, + CaseLet + >, + _ConditionalContent< + CaseLet, + CaseLet + > + > + >, + _ConditionalContent< + CaseLet, + Default<_ExhaustivityCheckView> + > + > + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + content.value.3 + content.value.4 + content.value.5 + content.value.6 + content.value.7 + content.value.8 + Default { _ExhaustivityCheckView(file: file, line: line) } + } + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + public struct _ExhaustivityCheckView: View { + @EnvironmentObject private var store: StoreObservableObject + let file: StaticString + let line: UInt + + public var body: some View { + #if DEBUG + let message = """ + Warning: SwitchStore.body@\(self.file):\(self.line) + + "\(debugCaseOutput(self.store.wrappedValue.state))" was encountered by a \ + "SwitchStore" that does not handle this case. + + Make sure that you exhaustively provide a "CaseLet" view for each case in "\(State.self)", \ + or provide a "Default" view at the end of the "SwitchStore". + """ + return VStack(spacing: 17) { + #if os(macOS) + Text("⚠️") + #else + Image(systemName: "exclamationmark.triangle.fill") + .font(.largeTitle) + #endif + + Text(message) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.white) + .padding() + .background(Color.red.edgesIgnoringSafeArea(.all)) + .onAppear { + breakpoint( + """ + --- + \(message) + --- + """ + ) + } + #else + return EmptyView() + #endif + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + private class StoreObservableObject: ObservableObject { + let wrappedValue: Store + + init(store: Store) { + self.wrappedValue = store + } + } + + private func enumTag(_ `case`: Case) -> UInt32? { + let metadataPtr = unsafeBitCast(type(of: `case`), to: UnsafeRawPointer.self) + let kind = metadataPtr.load(as: Int.self) + let isEnumOrOptional = kind == 0x201 || kind == 0x202 + guard isEnumOrOptional else { return nil } + let vwtPtr = (metadataPtr - MemoryLayout.size).load(as: UnsafeRawPointer.self) + let vwt = vwtPtr.load(as: EnumValueWitnessTable.self) + return withUnsafePointer(to: `case`) { vwt.getEnumTag($0, metadataPtr) } + } + + private struct EnumValueWitnessTable { + let f1, f2, f3, f4, f5, f6, f7, f8: UnsafeRawPointer + let f9, f10: Int + let f11, f12: UInt32 + let getEnumTag: @convention(c) (UnsafeRawPointer, UnsafeRawPointer) -> UInt32 + let f13, f14: UnsafeRawPointer + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/TextState.swift b/Sources/ComposableArchitecture/SwiftUI/TextState.swift new file mode 100644 index 0000000..c108acd --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/TextState.swift @@ -0,0 +1,337 @@ +#if canImport(SwiftUI) + import SwiftUI + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + public struct TextState: Equatable, Hashable { + fileprivate var modifiers: [Modifier] = [] + fileprivate let storage: Storage + + fileprivate enum Modifier: Equatable, Hashable { + case baselineOffset(CGFloat) + case bold + case font(Font?) + case fontWeight(Font.Weight?) + case foregroundColor(Color?) + case italic + case kerning(CGFloat) + case strikethrough(active: Bool, color: Color?) + case tracking(CGFloat) + case underline(active: Bool, color: Color?) + } + + fileprivate enum Storage: Equatable, Hashable { + indirect case concatenated(TextState, TextState) + case localized( + LocalizedStringKey, tableName: String?, bundle: Bundle?, comment: StaticString?) + case verbatim(String) + + static func == (lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.concatenated(l1, l2), .concatenated(r1, r2)): + return l1 == r1 && l2 == r2 + + case let (.localized(lk, lt, lb, lc), .localized(rk, rt, rb, rc)): + return lk.formatted(tableName: lt, bundle: lb, comment: lc) + == rk.formatted(tableName: rt, bundle: rb, comment: rc) + + case let (.verbatim(lhs), .verbatim(rhs)): + return lhs == rhs + + case let (.localized(key, tableName, bundle, comment), .verbatim(string)), + let (.verbatim(string), .localized(key, tableName, bundle, comment)): + return key.formatted(tableName: tableName, bundle: bundle, comment: comment) == string + + // NB: We do not attempt to equate concatenated cases. + default: + return false + } + } + + func hash(into hasher: inout Hasher) { + enum Key { + case concatenated + case localized + case verbatim + } + + switch self { + case let (.concatenated(first, second)): + hasher.combine(Key.concatenated) + hasher.combine(first) + hasher.combine(second) + + case let .localized(key, tableName, bundle, comment): + hasher.combine(Key.localized) + hasher.combine(key.formatted(tableName: tableName, bundle: bundle, comment: comment)) + + case let .verbatim(string): + hasher.combine(Key.verbatim) + hasher.combine(string) + } + } + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension TextState { + public init(verbatim content: String) { + self.storage = .verbatim(content) + } + + @_disfavoredOverload + public init(_ content: S) where S: StringProtocol { + self.init(verbatim: String(content)) + } + + public init( + _ key: LocalizedStringKey, + tableName: String? = nil, + bundle: Bundle? = nil, + comment: StaticString? = nil + ) { + self.storage = .localized(key, tableName: tableName, bundle: bundle, comment: comment) + } + + public static func + (lhs: Self, rhs: Self) -> Self { + .init(storage: .concatenated(lhs, rhs)) + } + + public func baselineOffset(_ baselineOffset: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.baselineOffset(baselineOffset)) + return `self` + } + + public func bold() -> Self { + var `self` = self + `self`.modifiers.append(.bold) + return `self` + } + + public func font(_ font: Font?) -> Self { + var `self` = self + `self`.modifiers.append(.font(font)) + return `self` + } + + public func fontWeight(_ weight: Font.Weight?) -> Self { + var `self` = self + `self`.modifiers.append(.fontWeight(weight)) + return `self` + } + + public func foregroundColor(_ color: Color?) -> Self { + var `self` = self + `self`.modifiers.append(.foregroundColor(color)) + return `self` + } + + public func italic() -> Self { + var `self` = self + `self`.modifiers.append(.italic) + return `self` + } + + public func kerning(_ kerning: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.kerning(kerning)) + return `self` + } + + public func strikethrough(_ active: Bool = true, color: Color? = nil) -> Self { + var `self` = self + `self`.modifiers.append(.strikethrough(active: active, color: color)) + return `self` + } + + public func tracking(_ tracking: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.tracking(tracking)) + return `self` + } + + public func underline(_ active: Bool = true, color: Color? = nil) -> Self { + var `self` = self + `self`.modifiers.append(.underline(active: active, color: color)) + return `self` + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension Text { + public init(_ state: TextState) { + let text: Text + switch state.storage { + case let .concatenated(first, second): + text = Text(first) + Text(second) + case let .localized(content, tableName, bundle, comment): + text = .init(content, tableName: tableName, bundle: bundle, comment: comment) + case let .verbatim(content): + text = .init(verbatim: content) + } + self = state.modifiers.reduce(text) { text, modifier in + switch modifier { + case let .baselineOffset(baselineOffset): + return text.baselineOffset(baselineOffset) + case .bold: + return text.bold() + case let .font(font): + return text.font(font) + case let .fontWeight(weight): + return text.fontWeight(weight) + case let .foregroundColor(color): + return text.foregroundColor(color) + case .italic: + return text.italic() + case let .kerning(kerning): + return text.kerning(kerning) + case let .strikethrough(active, color): + return text.strikethrough(active, color: color) + case let .tracking(tracking): + return text.tracking(tracking) + case let .underline(active, color): + return text.underline(active, color: color) + } + } + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension TextState: View { + public var body: some View { + Text(self) + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension String { + public init(state: TextState, locale: Locale? = nil) { + switch state.storage { + case let .concatenated(lhs, rhs): + self = String(state: lhs, locale: locale) + String(state: rhs, locale: locale) + + case let .localized(key, tableName, bundle, comment): + self = key.formatted( + locale: locale, + tableName: tableName, + bundle: bundle, + comment: comment + ) + + case let .verbatim(string): + self = string + } + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension LocalizedStringKey: CustomDebugOutputConvertible { + // NB: `LocalizedStringKey` conforms to `Equatable` but returns false for equivalent format + // strings. To account for this we reflect on it to extract and string-format its storage. + func formatted( + locale: Locale? = nil, + tableName: String? = nil, + bundle: Bundle? = nil, + comment: StaticString? = nil + ) -> String { + let children = Array(Mirror(reflecting: self).children) + let key = children[0].value as! String + let arguments: [CVarArg] = Array(Mirror(reflecting: children[2].value).children) + .compactMap { + let children = Array(Mirror(reflecting: $0.value).children) + let value: Any + let formatter: Formatter? + // `LocalizedStringKey.FormatArgument` differs depending on OS/platform. + if children[0].label == "storage" { + (value, formatter) = + Array(Mirror(reflecting: children[0].value).children)[0].value as! (Any, Formatter?) + } else { + value = children[0].value + formatter = children[1].value as? Formatter + } + return formatter?.string(for: value) ?? value as! CVarArg + } + + let format = NSLocalizedString( + key, + tableName: tableName, + bundle: bundle ?? .main, + value: "", + comment: comment.map(String.init) ?? "" + ) + return String(format: format, locale: locale, arguments: arguments) + } + + public var debugOutput: String { + self.formatted().debugDescription + } + } + + @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) + extension TextState: CustomDebugOutputConvertible { + public var debugOutput: String { + func debugOutputHelp(_ textState: Self) -> String { + var output: String + switch textState.storage { + case let .concatenated(lhs, rhs): + output = debugOutputHelp(lhs) + debugOutputHelp(rhs) + case let .localized(key, tableName, bundle, comment): + output = key.formatted(tableName: tableName, bundle: bundle, comment: comment) + case let .verbatim(string): + output = string + } + for modifier in textState.modifiers { + switch modifier { + case let .baselineOffset(baselineOffset): + output = "\(output)" + case .bold, .fontWeight(.some(.bold)): + output = "**\(output)**" + case .font(.some): + break // TODO: capture Font description using DSL similar to TextState and print here + case let .fontWeight(.some(weight)): + func describe(weight: Font.Weight) -> String { + switch weight { + case .black: return "black" + case .bold: return "bold" + case .heavy: return "heavy" + case .light: return "light" + case .medium: return "medium" + case .regular: return "regular" + case .semibold: return "semibold" + case .thin: return "thin" + default: return "\(weight)" + } + } + output = "\(output)" + case let .foregroundColor(.some(color)): + output = "\(output)" + case .italic: + output = "_\(output)_" + case let .kerning(kerning): + output = "\(output)" + case let .strikethrough(active: true, color: .some(color)): + output = "\(output)" + case .strikethrough(active: true, color: .none): + output = "~~\(output)~~" + case let .tracking(tracking): + output = "\(output)" + case let .underline(active: true, color): + output = "\(output)" + case .font(.none), + .fontWeight(.none), + .foregroundColor(.none), + .strikethrough(active: false, color: _), + .underline(active: false, color: _): + break + } + } + return output + } + + return #""" + \#(Self.self)( + \#(debugOutputHelp(self).indent(by: 2)) + ) + """# + } + } +#endif diff --git a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift new file mode 100644 index 0000000..a795b71 --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift @@ -0,0 +1,181 @@ +#if canImport(Combine) && canImport(SwiftUI) +import Combine +import SwiftUI + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +public struct WithViewStore { + private let content: (ViewStore) -> Content +#if DEBUG + private let file: StaticString + private let line: UInt + private var prefix: String? + private var previousState: (State) -> State? +#endif + @ObservedObject private var viewStore: ViewStore + + fileprivate init( + store: Store, + removeDuplicates isDuplicate: @escaping (State, State) -> Bool, + file: StaticString = #fileID, + line: UInt = #line, + content: @escaping (ViewStore) -> Content + ) { + self.content = content +#if DEBUG + self.file = file + self.line = line + var previousState: State? = nil + self.previousState = { currentState in + defer { previousState = currentState } + return previousState + } +#endif + self.viewStore = ViewStore(store, removeDuplicates: isDuplicate) + } + + public func debug(_ prefix: String = "") -> Self { + var view = self +#if DEBUG + view.prefix = prefix +#endif + return view + } + + fileprivate var _body: Content { +#if DEBUG + if let prefix = self.prefix { + let difference = + self.previousState(self.viewStore.state) + .map { + debugDiff($0, self.viewStore.state).map { "(Changed state)\n\($0)" } + ?? "(No difference in state detected)" + } + ?? "(Initial state)\n\(debugOutput(self.viewStore.state, indent: 2))" + func typeName(_ type: Any.Type) -> String { + var name = String(reflecting: type) + if let index = name.firstIndex(of: ".") { + name.removeSubrange(...index) + } + return name + } + print( + """ + \(prefix.isEmpty ? "" : "\(prefix): ")\ + WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\ + @\(self.file):\(self.line) \(difference) + """ + ) + } +#endif + return self.content(self.viewStore) + } +} + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension WithViewStore: View where Content: View { + public init( + _ store: Store, + removeDuplicates isDuplicate: @escaping (State, State) -> Bool, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping (ViewStore) -> Content + ) { + self.init( + store: store, + removeDuplicates: isDuplicate, + file: file, + line: line, + content: content + ) + } + + public var body: Content { + self._body + } +} + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension WithViewStore where State: Equatable, Content: View { + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping (ViewStore) -> Content + ) { + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) + } +} + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension WithViewStore where State == Void, Content: View { + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @ViewBuilder content: @escaping (ViewStore) -> Content + ) { + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) + } +} + +@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +extension WithViewStore: DynamicViewContent where State: Collection, Content: DynamicViewContent { + public typealias Data = State + + public var data: State { + self.viewStore.state + } +} +#endif + +#if canImport(Combine) && canImport(SwiftUI) && compiler(>=5.3) +import SwiftUI + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +extension WithViewStore: Scene where Content: Scene { + public init( + _ store: Store, + removeDuplicates isDuplicate: @escaping (State, State) -> Bool, + file: StaticString = #fileID, + line: UInt = #line, + @SceneBuilder content: @escaping (ViewStore) -> Content + ) { + self.init( + store: store, + removeDuplicates: isDuplicate, + file: file, + line: line, + content: content + ) + } + + public var body: Content { + self._body + } +} + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +extension WithViewStore where State: Equatable, Content: Scene { + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @SceneBuilder content: @escaping (ViewStore) -> Content + ) { + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) + } +} + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +extension WithViewStore where State == Void, Content: Scene { + public init( + _ store: Store, + file: StaticString = #fileID, + line: UInt = #line, + @SceneBuilder content: @escaping (ViewStore) -> Content + ) { + self.init(store, removeDuplicates: ==, file: file, line: line, content: content) + } +} + +#endif diff --git a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift new file mode 100644 index 0000000..4984029 --- /dev/null +++ b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift @@ -0,0 +1,22 @@ +import ReactiveSwift + +extension Store { + @discardableResult + public func ifLet( + then unwrap: @escaping (Store) -> Void, + else: @escaping () -> Void = {} + ) -> Disposable where State == Wrapped? { + return self.state + .skipRepeats({($0 != nil) == ($1 != nil)}) + .producer.startWithValues { state in + if var state = state { + unwrap(self.scope { + state = $0 ?? state + return state + }) + } else { + `else`() + } + } + } +} diff --git a/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift b/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift new file mode 100644 index 0000000..d768bdd --- /dev/null +++ b/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift @@ -0,0 +1,177 @@ +#if canImport(UIKit) && !os(watchOS) +import UIKit +import ReactiveSwift + +extension Scheduler { + + public func animate( + withDuration duration: TimeInterval, + delay: TimeInterval = 0, + options animationOptions: UIView.AnimationOptions = [] + ) -> Scheduler { + UIKitAnimationScheduler( + scheduler: self, + params: .init( + duration: duration, + delay: delay, + options: animationOptions, + mode: .normal + ) + ) + } + + public func animate( + withDuration duration: TimeInterval, + delay: TimeInterval = 0, + usingSpringWithDamping dampingRatio: CGFloat, + initialSpringVelocity velocity: CGFloat, + options animationOptions: UIView.AnimationOptions + ) -> Scheduler { + UIKitAnimationScheduler( + scheduler: self, + params: .init( + duration: duration, + delay: delay, + options: animationOptions, + mode: .spring(dampingRatio: dampingRatio, velocity: velocity) + ) + ) + } +} + +extension DateScheduler { + + public func animate( + withDuration duration: TimeInterval, + delay: TimeInterval = 0, + options animationOptions: UIView.AnimationOptions = [] + ) -> DateScheduler { + UIKitAnimationDateScheduler( + scheduler: self, + params: .init( + duration: duration, + delay: delay, + options: animationOptions, + mode: .normal + ) + ) + } + + public func animate( + withDuration duration: TimeInterval, + delay: TimeInterval = 0, + usingSpringWithDamping dampingRatio: CGFloat, + initialSpringVelocity velocity: CGFloat, + options animationOptions: UIView.AnimationOptions + ) -> DateScheduler { + UIKitAnimationDateScheduler( + scheduler: self, + params: .init( + duration: duration, + delay: delay, + options: animationOptions, + mode: .spring(dampingRatio: dampingRatio, velocity: velocity) + ) + ) + } +} + +private struct AnimationParams { + let duration: TimeInterval + let delay: TimeInterval + let options: UIView.AnimationOptions + let mode: Mode + + enum Mode { + case normal + case spring(dampingRatio: CGFloat, velocity: CGFloat) + } +} + +public final class UIKitAnimationScheduler: Scheduler { + private let scheduler: Scheduler + private let params: AnimationParams + + fileprivate init(scheduler: Scheduler, params: AnimationParams) { + self.scheduler = scheduler + self.params = params + } + + public func schedule(_ action: @escaping () -> Void) -> Disposable? { + scheduler.schedule { [params = self.params] in + switch params.mode { + case .normal: + UIView.animate( + withDuration: params.duration, + delay: params.duration, + options: params.options, + animations: action + ) + case let .spring(dampingRatio, velocity): + UIView.animate( + withDuration: params.duration, + delay: params.delay, + usingSpringWithDamping: dampingRatio, + initialSpringVelocity: velocity, + options: params.options, + animations: action + ) + } + } + } +} + +public final class UIKitAnimationDateScheduler: DateScheduler { + public var currentDate: Date { + scheduler.currentDate + } + + private let scheduler: DateScheduler + private let params: AnimationParams + + fileprivate init(scheduler: DateScheduler, params: AnimationParams) { + self.scheduler = scheduler + self.params = params + } + + private func animatedAction(_ action: @escaping () -> Void) -> () -> Void { + { [params = self.params] in + switch params.mode { + case .normal: + UIView.animate( + withDuration: params.duration, + delay: params.duration, + options: params.options, + animations: action + ) + case let .spring(dampingRatio, velocity): + UIView.animate( + withDuration: params.duration, + delay: params.delay, + usingSpringWithDamping: dampingRatio, + initialSpringVelocity: velocity, + options: params.options, + animations: action + ) + } + } + } + + public func schedule(_ action: @escaping () -> Void) -> Disposable? { + scheduler.schedule(animatedAction(action)) + } + + public func schedule(after date: Date, action: @escaping () -> Void) -> Disposable? { + scheduler.schedule(after: date, action: animatedAction(action)) + } + + public func schedule( + after date: Date, interval: DispatchTimeInterval, leeway: DispatchTimeInterval, + action: @escaping () -> Void + ) -> Disposable? { + scheduler.schedule( + after: date, interval: interval, leeway: leeway, action: animatedAction(action)) + } +} + +#endif diff --git a/Sources/ComposableArchitecture/UIKit/UIViewRepresented.swift b/Sources/ComposableArchitecture/UIKit/UIViewRepresented.swift new file mode 100644 index 0000000..bd123de --- /dev/null +++ b/Sources/ComposableArchitecture/UIKit/UIViewRepresented.swift @@ -0,0 +1,21 @@ +#if canImport(UIKit) && canImport(SwiftUI) +import SwiftUI +import UIKit + +public struct UIViewRepresented: UIViewRepresentable where UIViewType: UIView { + public let makeUIView: (Context) -> UIViewType + public let updateUIView: (UIViewType, Context) -> Void = { _, _ in } + + public init(makeUIView: @escaping (Context) -> UIViewType) { + self.makeUIView = makeUIView + } + + public func makeUIView(context: Context) -> UIViewType { + self.makeUIView(context) + } + + public func updateUIView(_ uiView: UIViewType, context: Context) { + self.updateUIView(uiView, context) + } +} +#endif diff --git a/Sources/ComposableArchitecture/ViewStore.swift b/Sources/ComposableArchitecture/ViewStore.swift new file mode 100644 index 0000000..de07f1c --- /dev/null +++ b/Sources/ComposableArchitecture/ViewStore.swift @@ -0,0 +1,161 @@ +#if canImport(Combine) +import Combine +#endif +#if canImport(SwiftUI) +import SwiftUI +#endif +import ReactiveSwift + +@dynamicMemberLookup +public final class ViewStore { +#if canImport(Combine) + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public private(set) lazy var objectWillChange = ObservableObjectPublisher() +#endif + + private let _send: (Action) -> Void + fileprivate var _state: MutableProperty + private var viewDisposable: Disposable? + private let (lifetime, token) = Lifetime.make() + + public init( + _ store: Store, + removeDuplicates isDuplicate: @escaping (State, State) -> Bool + ) { + self._send = { store.send($0) } + self._state = MutableProperty(store.state.value) + + self.viewDisposable = store.state.producer + .skipRepeats(isDuplicate) + .startWithValues { [weak self] in + guard let self = self else { return } +#if canImport(Combine) + if #available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) { + self.objectWillChange.send() + self._state.value = $0 + } +#endif + self._state.value = $0 + } + } + + public var publisher: StorePublisher { + StorePublisher(viewStore: self) + } + + public var state: State { + self._state.value + } + + public func makeBindingTarget(_ action: @escaping (ViewStore, U) -> Void) -> BindingTarget { + return BindingTarget(on: UIScheduler(), lifetime: lifetime) { [weak self] value in + if let self = self { + action(self, value) + } + } + } + + public var action: BindingTarget { + makeBindingTarget { + $0.send($1) + } + } + + public subscript(dynamicMember keyPath: KeyPath) -> LocalState { + self.state[keyPath: keyPath] + } + + public func send(_ action: Action) { + self._send(action) + } + +#if canImport(SwiftUI) + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func binding( + get: @escaping (State) -> LocalState, + send localStateToViewAction: @escaping (LocalState) -> Action + ) -> Binding { + ObservedObject(wrappedValue: self) + .projectedValue[get: .init(rawValue: get), send: .init(rawValue: localStateToViewAction)] + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func binding( + get: @escaping (State) -> LocalState, + send action: Action + ) -> Binding { + self.binding(get: get, send: { _ in action }) + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func binding( + send localStateToViewAction: @escaping (State) -> Action + ) -> Binding { + self.binding(get: { $0 }, send: localStateToViewAction) + } + + @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) + public func binding(send action: Action) -> Binding { + self.binding(send: { _ in action }) + } +#endif + + private subscript( + get state: HashableWrapper<(State) -> LocalState>, + send action: HashableWrapper<(LocalState) -> Action> + ) -> LocalState { + get { state.rawValue(self.state) } + set { self.send(action.rawValue(newValue)) } + } + + deinit { + viewDisposable?.dispose() + } +} + +extension ViewStore where State: Equatable { + public convenience init(_ store: Store) { + self.init(store, removeDuplicates: ==) + } +} + +extension ViewStore where State == Void { + public convenience init(_ store: Store) { + self.init(store, removeDuplicates: ==) + } +} + +#if canImport(Combine) +extension ViewStore: ObservableObject { +} +#endif + +@dynamicMemberLookup +public struct StorePublisher: SignalProducerConvertible { + public let upstream: Effect + public let viewStore: Any + + public var producer: Effect { + upstream + } + + fileprivate init(viewStore: ViewStore) { + self.viewStore = viewStore + self.upstream = viewStore._state.producer + } + + private init(upstream: Effect,viewStore: Any) { + self.upstream = upstream + self.viewStore = viewStore + } + + public subscript(dynamicMember keyPath: KeyPath) -> StorePublisher where LocalState: Equatable { + .init(upstream: self.upstream.map(keyPath).skipRepeats(), viewStore: self.viewStore) + } +} + +private struct HashableWrapper: Hashable { + let rawValue: Value + static func == (lhs: Self, rhs: Self) -> Bool { false } + func hash(into hasher: inout Hasher) {} +} diff --git a/Sources/swift-composable-architecture-benchmark/main.swift b/Sources/swift-composable-architecture-benchmark/main.swift new file mode 100644 index 0000000..26ca273 --- /dev/null +++ b/Sources/swift-composable-architecture-benchmark/main.swift @@ -0,0 +1,43 @@ +import Benchmark +import ComposableArchitecture + +let counterReducer = Reducer { state, action, _ in + if action { + state += 1 + } else { + state = 0 + } + return .none +} + +let store1 = Store(initialState: 0, reducer: counterReducer, environment: ()) +let store2 = store1.scope { $0 } +let store3 = store2.scope { $0 } +let store4 = store3.scope { $0 } + +let viewStore1 = ViewStore(store1) + +let viewStore2 = ViewStore(store2) +let viewStore3 = ViewStore(store3) +let viewStore4 = ViewStore(store4) + +benchmark("Scoping (1)") { + viewStore1.send(true) +} +viewStore1.send(false) + +benchmark("Scoping (2)") { + viewStore2.send(true) +} +viewStore1.send(false) + +benchmark("Scoping (3)") { + viewStore3.send(true) +} +viewStore1.send(false) + +benchmark("Scoping (4)") { + viewStore4.send(true) +} + +Benchmark.main() diff --git a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/DebugTests.swift b/Tests/ComposableArchitectureTests/DebugTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/DebugTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/EffectDebounceTests.swift b/Tests/ComposableArchitectureTests/EffectDebounceTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/EffectDebounceTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/EffectDeferredTests.swift b/Tests/ComposableArchitectureTests/EffectDeferredTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/EffectDeferredTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/EffectTests.swift b/Tests/ComposableArchitectureTests/EffectTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/EffectTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/EffectThrottleTests.swift b/Tests/ComposableArchitectureTests/EffectThrottleTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/EffectThrottleTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/LCRNG.swift b/Tests/ComposableArchitectureTests/LCRNG.swift new file mode 100644 index 0000000..c439e9d --- /dev/null +++ b/Tests/ComposableArchitectureTests/LCRNG.swift @@ -0,0 +1,15 @@ +/// A linear congruential random number generator. +public struct LCRNG: RandomNumberGenerator { + public var seed: UInt64 + + @inlinable + public init(seed: UInt64) { + self.seed = seed + } + + @inlinable + public mutating func next() -> UInt64 { + seed = 2_862_933_555_777_941_757 &* seed &+ 3_037_000_493 + return seed + } +} diff --git a/Tests/ComposableArchitectureTests/MemoryManagementTests.swift b/Tests/ComposableArchitectureTests/MemoryManagementTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/MemoryManagementTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/ReducerTests.swift b/Tests/ComposableArchitectureTests/ReducerTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/ReducerTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/TestStoreTests.swift b/Tests/ComposableArchitectureTests/TestStoreTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/TestStoreTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/TimerTests.swift b/Tests/ComposableArchitectureTests/TimerTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/TimerTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/ViewStoreTests.swift b/Tests/ComposableArchitectureTests/ViewStoreTests.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/ViewStoreTests.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift b/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift new file mode 100644 index 0000000..fecc4ab --- /dev/null +++ b/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift @@ -0,0 +1 @@ +import Foundation diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift new file mode 100644 index 0000000..8a04d7b --- /dev/null +++ b/Tests/LinuxMain.swift @@ -0,0 +1,2 @@ +// LinuxMain.swift +fatalError("Run the tests with `swift test --enable-test-discovery`.")