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 0000000..5563385 Binary files /dev/null and b/.swiftpm/xcode/package.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate differ 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 0000000..793131d Binary files /dev/null and b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate differ 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 0000000..186b90e Binary files /dev/null and b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ 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 0000000..186b90e Binary files /dev/null and b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png differ 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`.")