From b7123f667c6ad50a6445badcc4e62293ec00c6e9 Mon Sep 17 00:00:00 2001 From: Mike Packard Date: Wed, 12 Jan 2022 05:45:36 +0700 Subject: [PATCH] update code Signed-off-by: Mike Packard --- .../UserInterfaceState.xcuserstate | Bin 11762 -> 25194 bytes .../contents.xcworkspacedata | 6 - .../xcshareddata/swiftpm/Package.resolved | 44 +- .../IDEFindNavigatorScopes.plist | 5 +- .../UserInterfaceState.xcuserstate | Bin 272511 -> 367307 bytes .../CaseStudies.xcodeproj/project.pbxproj | 772 ------- .../contents.xcworkspacedata | 7 - .../xcschemes/CaseStudies(SwiftUI).xcscheme | 78 - .../xcschemes/CaseStudies(UIKit).xcscheme | 78 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/AppIcon.png | Bin 8152 -> 0 bytes .../AppIcon.appiconset/Contents.json | 99 - .../Assets.xcassets/Contents.json | 6 - .../SwiftUICaseStudies/ContentView.swift | 14 - .../Preview Assets.xcassets/Contents.json | 6 - .../SwiftUICaseStudiesApp.swift | 10 - .../UIKitCaseStudies/AppDelegate.swift | 7 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/AppIcon.png | Bin 8152 -> 0 bytes .../AppIcon.appiconset/Contents.json | 99 - .../Assets.xcassets/Contents.json | 6 - .../AuthScreen/AuthAction.swift | 11 - .../AuthScreen/AuthEnvironment.swift | 6 - .../AuthScreen/AuthReducer.swift | 22 - .../AuthScreen/AuthState.swift | 6 - .../AuthScreen/AuthViewController.swift | 117 - .../Base.lproj/LaunchScreen.storyboard | 25 - .../Base.lproj/Main.storyboard | 76 - .../CounterScreen/CounterAction.swift | 11 - .../CounterScreen/CounterEnvironment.swift | 6 - .../CounterScreen/CounterReducer.swift | 24 - .../CounterScreen/CounterState.swift | 6 - .../CounterScreen/CounterViewController.swift | 129 -- .../CountersTableAction.swift | 10 - .../CountersTableEnvironment.swift | 6 - .../CountersTableReducer.swift | 22 - .../CountersTableState.swift | 11 - .../CountersTableViewController.swift | 133 -- .../EagerNavigationAction.swift | 12 - .../EagerNavigationEnvironment.swift | 6 - .../EagerNavigationReducer.swift | 38 - .../EagerNavigationState.swift | 7 - .../EagerNavigationViewController.swift | 144 -- .../CaseStudies/UIKitCaseStudies/Info.plist | 25 - .../ActivityIndicatorViewController.swift | 20 - .../Internal/IfLetStoreController.swift | 50 - .../LazyNavigation/LazyNavigationAction.swift | 12 - .../LazyNavigationEnvironment.swift | 6 - .../LazyNavigationReducer.swift | 38 - .../LazyNavigation/LazyNavigationState.swift | 7 - .../LazyNavigationViewController.swift | 146 -- .../MainScreen/MainAction.swift | 11 - .../MainScreen/MainEnvironment.swift | 6 - .../MainScreen/MainReducer.swift | 22 - .../MainScreen/MainState.swift | 6 - .../MainScreen/MainViewController.swift | 166 -- .../RootScreen/RootAction.swift | 11 - .../RootScreen/RootEnvironment.swift | 6 - .../RootScreen/RootReducer.swift | 29 - .../RootScreen/RootState.swift | 13 - .../RootScreen/RootViewController.swift | 126 -- .../UIKitCaseStudies/SceneDelegate.swift | 13 - .../AccentColor.colorset/Contents.json | 11 - .../AppIcon.appiconset/Contents.json | 148 -- .../Shared/Assets.xcassets/Contents.json | 6 - .../TodoApp/AuthScreen/AuthAction.swift | 10 - .../TodoApp/AuthScreen/AuthEnvironment.swift | 6 - .../TodoApp/AuthScreen/AuthReducer.swift | 20 - .../Shared/TodoApp/AuthScreen/AuthState.swift | 6 - .../Shared/TodoApp/AuthScreen/AuthView.swift | 89 - .../TodoApp/CounterScreen/CounterAction.swift | 10 - .../CounterScreen/CounterEnvironment.swift | 6 - .../CounterScreen/CounterReducer.swift | 21 - .../TodoApp/CounterScreen/CounterState.swift | 6 - .../TodoApp/CounterScreen/CounterView.swift | 99 - .../TodoApp/MainScreen/MainAction.swift | 21 - .../TodoApp/MainScreen/MainEnvironment.swift | 6 - .../TodoApp/MainScreen/MainReducer.swift | 99 - .../Shared/TodoApp/MainScreen/MainState.swift | 9 - .../Shared/TodoApp/MainScreen/MainView.swift | 193 -- .../Todos/Shared/TodoApp/Models/Todo.swift | 7 - .../TodoApp/RootScreen/RootAction.swift | 10 - .../TodoApp/RootScreen/RootEnvironment.swift | 6 - .../TodoApp/RootScreen/RootReducer.swift | 27 - .../Shared/TodoApp/RootScreen/RootState.swift | 13 - .../Shared/TodoApp/RootScreen/RootView.swift | 90 - .../Todos/Shared/TodoApp/View+/View+.swift | 9 - Examples/Todos/Shared/TodosApp.swift | 13 - .../Todos/Todos.xcodeproj/project.pbxproj | 750 ------- .../contents.xcworkspacedata | 7 - .../xcshareddata/IDEWorkspaceChecks.plist | 8 - .../xcschemes/Todos (iOS).xcscheme | 78 - .../xcschemes/Todos (macOS).xcscheme | 78 - Examples/Todos/macOS/macOS.entitlements | 14 - Package.resolved | 20 +- Package.swift | 2 +- README.md | 4 +- .../Beta/Concurrency.swift | 103 - .../Combine+ReactiveSwift/AnyDisposable.swift | 12 + .../Combine+ReactiveSwift.swift | 12 +- .../Debugging/ReducerDebugging.swift | 137 +- .../Debugging/ReducerInstrumentation.swift | 44 +- Sources/ComposableArchitecture/Effect.swift | 310 ++- .../Effects/Cancellation.swift | 51 +- .../Effects/Concurrency.swift | 109 + .../Effects/Debouncing.swift | 23 + .../Effects/Deferring.swift | 15 + .../Effects/Throttling.swift | 11 + .../Effects/Timer.swift | 93 + .../Internal/Binding+IsPresent.swift | 13 + .../Internal/Breakpoint.swift | 32 - .../Internal/Create.swift | 176 ++ .../Internal/CurrentValueRelay.swift | 56 + .../Internal/Debug.swift | 208 +- .../Internal/Deprecations.swift | 1042 +++++---- .../Internal/Locking.swift | 16 +- .../Internal/RuntimeWarnings.swift | 26 + Sources/ComposableArchitecture/Reducer.swift | 849 ++++++- Sources/ComposableArchitecture/Store.swift | 444 +++- .../SwiftUI/ActionSheet.swift | 96 - .../SwiftUI/ActionWrappingScheduler.swift | 107 - .../SwiftUI/Alert.swift | 570 +++-- .../SwiftUI/Animation.swift | 20 +- .../SwiftUI/Binding.swift | 370 +++- .../SwiftUI/ConfirmationDialog.swift | 330 +++ .../SwiftUI/ForEachStore.swift | 83 +- .../SwiftUI/Identified.swift | 27 + .../SwiftUI/IfLetStore.swift | 135 +- .../SwiftUI/SwitchStore.swift | 1962 +++++++++-------- .../SwiftUI/TextState.swift | 732 +++--- .../SwiftUI/WithViewStore.swift | 169 +- .../TestSupport/FailingEffect.swift | 89 + .../TestSupport/TestStore.swift | 493 +++++ .../UIKit/AlertStateUIKit.swift | 108 + .../UIKit/IfLetUIKit.swift | 47 +- .../UIKit/UIKitAnimationScheduler.swift | 346 +-- .../ComposableArchitecture/ViewStore.swift | 539 ++++- .../BindingTests.swift | 39 + .../ComposableArchitectureTests.swift | 163 +- .../DebugTests.swift | 206 +- .../EffectCancellationTests.swift | 306 ++- .../EffectDebounceTests.swift | 87 +- .../EffectDeferredTests.swift | 84 +- .../EffectTests.swift | 288 ++- .../EffectThrottleTests.swift | 210 +- .../MemoryManagementTests.swift | 38 +- .../ReducerTests.swift | 226 +- .../StoreTests.swift | 475 +++- .../TestStoreTests.swift | 61 +- .../TimerTests.swift | 123 +- .../ViewStoreTests.swift | 255 ++- .../WithViewStoreAppTest.swift | 39 +- Tests/LinuxMain.swift | 2 - 153 files changed, 9654 insertions(+), 7655 deletions(-) rename Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist => ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/IDEFindNavigatorScopes.plist (72%) delete mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj delete mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(SwiftUI).xcscheme delete mode 100644 Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(UIKit).xcscheme delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/Preview Content/Preview Assets.xcassets/Contents.json delete mode 100644 Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/AppDelegate.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/AppIcon.png delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Info.plist delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootAction.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootEnvironment.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootState.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootViewController.swift delete mode 100644 Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift delete mode 100644 Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json delete mode 100644 Examples/Todos/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json delete mode 100644 Examples/Todos/Shared/Assets.xcassets/Contents.json delete mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift delete mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthEnvironment.swift delete mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift delete mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthState.swift delete mode 100644 Examples/Todos/Shared/TodoApp/AuthScreen/AuthView.swift delete mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterAction.swift delete mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterEnvironment.swift delete mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift delete mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterState.swift delete mode 100644 Examples/Todos/Shared/TodoApp/CounterScreen/CounterView.swift delete mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainAction.swift delete mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainEnvironment.swift delete mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift delete mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainState.swift delete mode 100644 Examples/Todos/Shared/TodoApp/MainScreen/MainView.swift delete mode 100644 Examples/Todos/Shared/TodoApp/Models/Todo.swift delete mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootAction.swift delete mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootEnvironment.swift delete mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift delete mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootState.swift delete mode 100644 Examples/Todos/Shared/TodoApp/RootScreen/RootView.swift delete mode 100644 Examples/Todos/Shared/TodoApp/View+/View+.swift delete mode 100644 Examples/Todos/Shared/TodosApp.swift delete mode 100644 Examples/Todos/Todos.xcodeproj/project.pbxproj delete mode 100644 Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata delete mode 100644 Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist delete mode 100644 Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme delete mode 100644 Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme delete mode 100644 Examples/Todos/macOS/macOS.entitlements delete mode 100644 Sources/ComposableArchitecture/Beta/Concurrency.swift create mode 100644 Sources/ComposableArchitecture/Combine+ReactiveSwift/AnyDisposable.swift rename Sources/ComposableArchitecture/{Beta => Combine+ReactiveSwift}/Combine+ReactiveSwift.swift (73%) create mode 100644 Sources/ComposableArchitecture/Effects/Concurrency.swift create mode 100644 Sources/ComposableArchitecture/Internal/Binding+IsPresent.swift delete mode 100644 Sources/ComposableArchitecture/Internal/Breakpoint.swift create mode 100644 Sources/ComposableArchitecture/Internal/Create.swift create mode 100644 Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift create mode 100644 Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift delete mode 100644 Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift delete mode 100644 Sources/ComposableArchitecture/SwiftUI/ActionWrappingScheduler.swift create mode 100644 Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift create mode 100644 Sources/ComposableArchitecture/TestSupport/FailingEffect.swift create mode 100644 Sources/ComposableArchitecture/TestSupport/TestStore.swift create mode 100644 Sources/ComposableArchitecture/UIKit/AlertStateUIKit.swift create mode 100644 Tests/ComposableArchitectureTests/BindingTests.swift delete mode 100644 Tests/LinuxMain.swift diff --git a/.swiftpm/xcode/package.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate b/.swiftpm/xcode/package.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate index 556338554164076db2c21ed454183354c223023e..35021cd0de6573036bacd59bbfd4f7a7d2d16bb9 100644 GIT binary patch literal 25194 zcmeHvd0bRgANM(TLqwQi7#Ie|VPIeYl^H+;TtJ2ymSGuS2HcfK7;zLBWCpjg?#)tD zTTHXn1>BO%%1X-?H8aa{Ps=jR%(N_Rv&FLB-<>zp{n zUgLI25T3WXS}ZbKi|w=QwR&%%!>AYXud=#rZaa&g3MQga0P2HeC>+U=4ke=$l!^wT zG&BgMqYRXZ2BYC92aQ4#P&Kk62bzK!krTO)8_h(s&>VCtx*g3$^U(sd7~PGQp!-of zdID`iPomA}DfBdY25mvlqUX>HXb0MfcB9wPKJ+#^irz)X&~bDEokSm@bLca49({oc zOkxVtn8ALy7xu@!aRBav#aM>>VkOq#c$|O}aSBewMYtH3;8JYHWw;zyU<_ON8?h5N;hA_AUV`t(OYt)N0A7wC#1G+z@gw+Ayb7G!#nU!{60Q` zKfoX2llUWi3ZKRw<1_dZ{2BfVU&5F175oGK4gXFc0uuz$n+PD(L@W_UXoz?sfk-6! z5&ek)L=urgWDnbKXH_JmpDm$M4Tc% zBhC}QlZeD5L6Rgz(j-Irk-bQNGLV##5o9E(CS%DsQbWd*Ix?9oBu!)ySxlCYrKFiG zBg@GO(n5|P$CEW=Em==Ckd5SYat1kvyp_C-e2{#Ie3*QMe3X2QTtTiRSCOm9RSE?k5kB2gyU^2jqw3N%CXzQ}TQA z3i$*1Bl#2gGkKN#jlvW`(Ud$>B006dMKSo=hFprA#J87 z&{OECbTd7Zo<+~5Z>4Xi=hE}(1@v9?-Sj>5GWsF5cT0^j7*6 z`gQtE`Yn1NeSkhlAEn==-=|N|AJM1i)AR-UGW|XM6a6!NjsA@x7?O!(6ii=6$wVfR_1o* z5#~|mF=hp`l3B&9W?GpxW(~8Jd7OEQd7jzIyvpog_A+lW?=btBcbN~F)6B=rr_4F# zbLI=?5_6gPj`^9n%KYx<@7LQez%SG<%unVQ=@(TM+t^ZHe-s6xAS6O!6oSG=nyuQY z_F1km@Yk!4M#R?C+gz?T6pSP+!IEuAib7e6!+JKmS}Zjg^F}(Gr@ESKHFl%3rlrB& z=&lyW4o=Ik#-}BL<-aw zDNz(sp=cC?)GW<1tRLHp^=Est0c;;Oa3hLC8WfKbP$KGw`lA6Ti49^!90qaxI4tLI zCWi-ecqoU5u?CPXx7lu++T?UJx?TDjw_`eJ&)RBngwa09)>7}zt97`Y%^scV%6B$b zSxQ{vJjLwRn#uMCJ2W7CwX`%E?Jl>Y(dGutT`f*7vANs^XG2rH9U3g>6}rm}4=z;8 zwYflV^W8bzvn=)|XETh5$Tv769gVfl87`1Q_@r-cwl&s4Z;fuFePT zYZGCp#=_rbdp!*8bUTY!DH~EPzI&3R-abU9(^a`(aCMD!Ewk*6O_QCCbvl@csbDX4 zFthv++g&ppliW!)Fd0slZDPGW$zxXCFnBG^cAfBWE*(Ev!hMfH_1*T2c%1{9wb*N0 zYV6IXMwh$U17goqYdx(WK{p{Anuuz~2y3Cy)?gnce6+i&%vP9%Hn?3o%4{w381b&p zIob0}Vb$U}|D$KBGQnKyOp{9NZqG;I8M=zp;1MajT0H-MBvOxI?zqrXxMq)V5!K>5 z{zt+I<5guE;jqtu)!J-#!7{Vg_LOXfM>0jVc>4cHGF~DpXefJ6S#I&jqO2C*^`B+2 z@+(Bw4flV`;`s~lIFER$YVpGVEZ*NDi*FJE`uavkgJ<#b+PJo*-rn4E_~v=!i>Ve@ z{ax~v+iIrT>aH_Wi#%P$R*ReeuCD%rPWuzY+~<))Q!RG?kL2)bRNvqWUdB?ik~b~O z&;w{WdJsK?9!8I#N6}+w1)IPovi;cp>;N{2)v`J^nN4X&tH9#7!p|DC7OjK78&DjZ z3N}9-elpnMY#RK{f$w1JB03G3VCb|ij}7%$Y(>wTlbth49XwW=s>RXQz0KQ1gA*b& znAmD@)OE|48mBv44zN35(R;R1Z=2<8ap%nh=YWs-pp3d_D>co|`g)HSqIFK1$^CkA6y(p{|y}@R-qBq&W zyw3>kSH0UN%uz}A0rJ&)2OUIVZD>C_zz$)DwxL7lFguLRVzVdIdVS0idt;q@@>qT- zOpUennRZujK#m~kduZEgUTWTGe}Fz5XSNz0;3_y9ZOtIu2dGw!y#IXNokC~$?oOkR zSv{NEh7O}c(Ah9HyJ`}Qm+KFm3N2qipI3`R%+}HtH}7_IcCiLs;5+fPeG&b@BmPVD z6}p5jqp#67=v(w1`W{_j^Vod0fGuQAY!O?`mawI)xgGuJMf@-5R}bQU2gH|o5nsWM z@F0HVzlis^?phZt;=c>=I1mtzgV=H};&JeG5$~N>9F8LZ*jUb5T5%+6y)j_pC>+Bh z?EsEuhq9G`G#m>^tLg#lU_!3m@wEU`UyuE8k_V-D0HAc#pV#%3ABeL7c{mLZ!s$2z zXX3$l2p)=u;VgCxJC+^Cj%O#Z)$C2Ijh)EWwBz9d@~}bBwYUI~SL;RIB(JWW`Y-bS z0pwNj$Q!}heaIXAm&m&bPvntjW9wRR4LkV;kynSO0DACbP^k`3sdy@=)G60dslfr8 zeE^z<-2&iT0JwS{a8seelpbL9m7k68AmH;AVOOH`}Y~Zu=K-{{Y|~;{msVo#g}E>c0fs2E366 z+~e%ct+<`NefVwmc6Kg%2YV+wkDbphVDDlVvWwdBelOq-<0JSe zYQ@I@aErZwyN`W?7=>>|^W-b|t%tUCp+# zZS0zMB3!^9p%5%Ap#uD^_2TbwuZ7+GFaG|1{1IBfAE9H{dGSZ20{)1BL>m9gu4gxR zeg}zT#1TF$3?;Gvf5b3$V=Ix(w%-{3h+HBM5JVUNe@_7Zhw3+#*R_I9Gy3&6=<0M-KlU-AO*6)yl^`xk)!003w60KA!f*$2Q| z|H|SLcM|h?@XcdiZ6y}4J8lqsi-~)A^c*1Wf#6~%s4rp(sIOf;&=V{f>;uqp;$Z=B z4*}qI|9KT(`Bg+a54hDtE73-*A=VP>i1ow<;&EalyN7*)-OIkozQyii-)7%o_p=Au zi6^{(d&&#AX8~{ry?{IF1>F1p0`4CG+%6t)ud#=GfZM}syf=uw{4aZ$J>vNtBn}ae z^v&%7;t&tIgY3Jl#9{W>4TA0%@xB1M_W*Rqc?5j`pnI<;==^5;Iyy~!B7p7;fbPVf zSMim=$5SNfo$w7q92yMf{}BiSfF4^SxLi6UhD^?2-KddzS%w zqzdX@>;YF_`4kd1Rolr_av+&T4kFXZ3^J1(Ob#K3vfr@Zvfr`avsc(3*dN)S*q_;} z?PQjKJyI_yJTeck_lp;M*93*fVd7u#{R7~$^5CmvfAxWH%p&|)rTP`*(_jg8ms3p$%D?pX7Sbjn9ugrm`Tp& zg`34;x|O_{!wj1}9yamKu<>klH+vHJVb|Z%_O-y0i%?iQ$&t5{bICi%JIQ(Ed~yMK z7rBtby*TX8;ock$;BX%f2XZ)w!@!~tb2zx2Tuk20W0<^;TtePYE+vpeTXUV=Z8a7HUqbk7v`=@`*vny0f0DDgVT{9-SMR9fH%Y3+ zVO>j@Tq7N@^>25ThmB{EbZf1|S>t72NQ9DIH#Doo-it!d`L+f}{j6$n zXxGk4Vf&+=-_q$Jgu&TZ(+o_GF%7mFr)y?Psy1bW@UhDCr&=6SJKbIDd8nj%kfYwi zH)wI&n}>M5PIkK7BW7l%SEr|yk8+gSYElLdNcGLcMshRKt|i;aC&*3YlN^rVa3qHn z9PYc8e2RRUe1_b@VI_y7IIQ9zFAQ5*VKCR-2_%&cH$QseJ#LH;*o4=+Ro29Bdce>o z_o(INuE1?;OG5+fiwLN)+Us~8imNKO#Zg~7B4wZRasU9Kj}_3YBk)4wJ{OC;V~p zJ@S1HCvvzShx@Yz^>A-Sz6Pn{2&6hfeuPBiDe^Q=WvR8fZDqmayAyrPPfU;$&G`NI zjL%;FmCa748|a>5c|e~KsiJRu!hrOQ%$UJhdHDr}rZQ{gsIguWS74Ax94r9>iuXYU zb_#5L>D{0&CIVDlK;8;x$7d0s*a*bA(ERez0qz+8u z31PrS3i0g1@$K<)b=0dCtISqIC4W8NZsWN?R=XP*Ty?H4MZ$eWs!UZb*yQcFbtEik z$gtL-px&f>-`V=ew&6Lfh!wZx8h}|P;d_mQ)_pDWTrFp_sTLNGW0J!Tc$9VAt(?_l z@4O>MV&+IuaY^a*xJ2EzDoe~Agd(U>UNNC#v(dB9x~AM>_U*X>VN7^deoQ@&0yU~e zfL8tEz_yJvU*{^69zDi8*=?iE@O9jH(5YSEQ!4Ff8m zbxV99t9ur2Vg5tON(b+OKuL8@FJXAZ_z3w6sKH;!YrHCPLjkpZ_kn?`$G&!RSlEUq zf2WWzMD{lFcLWs7;7Vt$ZI;UBR;8p2NljLjmwE=6A}Q*RFsDeKP~fpa6ay|E&`?Qs zkawu5-c%o+^a$5lDR5+lum+)|nB3{P3MNXpASHLM?)V}@+uC_PBXApYJsihEWS~@( zhtCMyMu~^x7{!9418&-IzPAqIBBkMpG#myZ!#_?QqWTMc<^2D}^YT!6C@qzav>PZL zl}x2jsnkF!jT*$^Tn-yJY~*krhx0jHz~RCTR0e9L22(?*RNk@Vu!;90i#S{irFf^3 zHB10}3QX7hW-uUvmmplIv`x&lHIKBpMhHBckq-A{E9mihJKU)jC-60W1Z1H`wb^O` zwyv|8=Vf?!HT>GF7H4-Y3YEd_bMw?2P}g}W`RS>K^z@|U!N!!Nv|PO*X>e{@ep3G6 z!RZ52atG&SW()!bkZ%>)J9_Nij9A|B9FPY{0Vix}U)?w89NxfO$jL*LNu&$mCY=qwS3yqhm4xk({5vW|%x! z*CtnIARCG^kh{=rYa>ZIaxIr ze)%7MPGL$14FW=>qRWNMp0{Ju(o+WxN=->l%1bq*0dam{X42ri)choUX3Ag#d^M!% zGpgWWhh#d%15q=a?XjU6IMw4o)8VrV&45pn;Cm8Wb)p8;@jvzP1PeiD>!hC7p? zlpDVDHCsHbs8mv<0*md2@_eh>&bGkdcU?KcAVF{i7kxbR&ZTq|JhMUR8QxO^qh@#} zjGty{QwL4H(;H%Yi*>C32>y9eGL8%xIxOpYZV?Qt+1kmnuQKH|@?5rBi+z&4+1^-V zZyT1~;VK=?(WmAbjJz%S!%2qKtJyx#M}9$fH!DATLh{UvNtuH((=###CZ{L&@MY@J zY&b2o9Blgz{=rC9IoT=6#m;(-USiEBwK-GWwuRs)CrLmf2rfE z9@x8J&EZm&+fnauJM0kB*SaSQHGxgvA^!?nD?s%|G9I(pM~=p>NVcebMAF1XJH&CZr4 z;RYWx`}&T}L85|zR;qe@v03Ub>q@ZUY zGvF~ixwH7J103XoUimX49X;}wAt4GCMyq7rvLPapS=Lx4zbTHghM=Qd9zh5j=Les_(*|i3zpzd zQe{;h(zuv90+BPsb3J9X&up@Jc)Omb83LW0<r%r8}C`120f<@YEuS?i6a@AgKxOzYrm9urf$ymYeT_M zv}I*=@P8qwSLaL<)Tlb=o&?XQ)?DbwkPp*q%==T>$97@ai~T$nBNA+87K9X|(0Grv zodz*)E7}gHXJ3O@_XG4f@P5BXKcnB^Tx>Mf02{LqPQZ=;3&Mh}cnEL6Ps7R97x1fa zu5}NbY(0;^h7+tmLnJ$d7*1GVQ-2DaNNt9*sIwr3eH8XypCq0po`n;qJBT-l!*I^@ zeK=+M14#(a(_t+Nyk^4thopo^>wan}xY>C#o9YEexva&_ld1Vb9iGhUbbjS~)z2Dg z8T9}kw%uU)YVpMCN_T-_L`u4GL}h0ma5^J3z4L3AF8Ns2mErBKdW5xlU`l5>&+@Z^ z1!}D8bRDX1K-c>;;6v0S5F}C$bJ*TWfwipR^>p0zErrJrXd&Ct;yFR4&S}c=jGV8n z)l?hb7KAaATd6f1cJOUQx4=2I@@C#2X|~rIK+)AXn`d?K{!K=KR8MUXg3u1o`5JwK z+J>}GP@AYHsm;_=)YH^6)E4Sl>N)CpYAc89Io!bEMh-hU+{EE&9B$^Yi^C9KgGrkH z1oZ-1M{TEGqF$z6pK;UyftpCfv6L?}lnIRbLc_wjOv zfbH-ehVjy^O%dez}_HhQcTe-zIfg9-Louwf#v?csCDd}3fs*PANu z7dUL`;In^_8+Aj5m(Ms0emc03oyOjC+cViNI4IF1l=I#8oRtTc&wD`2yB82Fs97+# zeA7I50T$jT>hu`9*Mi1XJ{Q2(y4~h!z;l$K9Bj^nQrk>N10)n7?*+5zo2Nq*B#PHk zhp8jfQR-a|&*U)d(ah%X&1I8>x;V|b(;@r&|MwQqndBx)WC&$36a#?IMJV|eO=7{chz2{4y!0s>RWzYd;^T%R_Z$r&*dcwhUMS!%pMyrBx8OgceYVKQ9pC|4h}Eu+(w{&r4b5S zLtUeOqkiY`ogALW;rVN5jN)jL!wY!7^e)&7gJwH5!8ra~XPhmDulxg)+Ugn|?v`46 z#~i40?p)nI2s2$`1ksci`aCo$ou0-$qe4fMJ6mbkT3OmTAaoo(08)vx zhK{Eb=tR07-JiqDIQ#&Imvi_*4nM@1pEBD$D{Fb8sc+d2FeJLrA7j4r1uXbTe2 zR=Sd|qDRmp(O7ykJ%%2O@@dF#%YBE51QiAghwoWPJ4AzHZs+qxJjS&y&!%g4H3HJO zc-_!>@&{I6qbE-Cw)DsH!QJli{fzZV07ryRPyG#o=`vUeDnTFdrVh1rYWg z!xZ=OxH|OB^evEh0rlJI*a*8Bo(9Toj^;Lc4)5OlWf`9sx1n`31brKM;|iVriU02_ zeFuFfU-}6SZ-U~!m_$tY?k^Nt+w5)a#zLE;^t~`1_n{f|{b+1g42|Z~?|{mNCKfiB zpJs*AdGrJHa#-Y`7k%^R!$ceXAphLgy9IF|{V@Fq{pgq`K#MS59DbI=TdKun|Ig?0 zg;SwT4Z4|pCP$d2mGr8yAfpe6ZS-nX+x3D_s*PSFWPQ7Vt8-?%FS<5*E&qUgmiUIs z!&o%6Ossd*2m&U#JTFK1zkQKHPbdPm{^J`4=yv)ENFRWx-PAF!liKJ_{9|1AUQb20 z*`w&50Y&#LDxx7ZvDd3Qz$XON@g}S8p*ptFFZfgkhu`4v%UxEqus&bq*XIs;C%ub) zjl-{ScqfP9!|R>va}WIn5A|0$yaS-#Jwo!q{cQp6yKZdp91<4K?(U1n*VZwitv&zM z(pkhW9X8gpNV}F{R?i_A?G2=I{{?@AC{Ue9h`Hx}Vcu zbdK&J4!_-FbiYR1+URfSZ#n!9hYxg)=@s%4Kc@RVrgFph?x)PXdhx0-4hR36aiBE( zIKXa}XCNRbs&O`GZTwEXw$|-iq)AP};r65kTccwV=ohbjXDCJlZZ# zl+NLgeMGrW1;_T^qunlWwf?Syu8anDQJHuSpYk&0n0~Ofi8q)5{4Wb=J}vw=NNn-V zbSjeuIYee4htIS!gE;)jjq&A}!OSq8`gVXB$`0l5S^h-B2N16GqzC&t%4PTi6zDW# zwcycFObIy93?#ZQ@Eg5MITA4yj0G~JH7yX9``(??#mVIG=N!?i=OPCM z%8Xz}!Xn??sX#e=g~MNX7Wv+u8k8B&Oz2$W-*folAJ;fj3!}`~nMq6?GnvC*a`+O5 zzvl3_9jl$0DrnHJdTLOw&SRQ{wSM`A^e59I=ue2rx;h`ewq^@$efQt!4b)YC`sQvf za}Tc$?qKd@<}ve`1VQCAbwCgTXPYCKf7QX?&zoh|@#x3VOX}zy{@Xhkm z%obi1Ji`%WEAuQzP&cRwwlUiURq!IH0)mE>4b|}Nes^B$H1%%(9SMWH7 z%v;PpVN)$HG3wFYM#Y zd(8XH36AK)5kVXw<_JmW!anI)*hJu;FYGg(#Yl*5Y+--qS=dDI^%wRmU*U7f{(J0Q(k0a!eZ6P8c z+tM*f>Kw;SU9&BIef-4NYakf4`h{?W0$3$PUmlDiXu_8l2d*f1>`6OUQ(BWhM?iVhZ_N}J)1zSg?AiogCy-fNFyGn-lsl*HyWOyKBYdRuEE<1 zQ)nZ+pRf?p871(B!gAUIsf!V`1Kv~EKsSNIHyz$pcnf_SysPjI`cZJ&_s}O95xiM2 zn<;0eGWGDrz+2#rfgCdzl<@P+YXHg<@J7Io;GKYHn6vOs!1M4{z>6>ygdgR{`1OJ} z0`~C>@)P?>{G{++K($|gzYM>@enb7T{Pz1D_dDbFh2PhHSNwkK70@fNm#9~8uO+=! z_FCPmt=C$At^ZK}EdSyDdjB{5kNLmnf5QL6-j%&=y=!{gd)M{;E&v4(0aO4Jury$G zKwH4tfb{{t^?~iXKB7LsedhI9+UJ2j5B7ODP#oAdFf}kOFg-9ca7f^=!0f=Bz}!G% zV1D4(!100AfwsV!Kzm?apd)ZLOTnmApYDJ~V4i7Uibag}(ac(iz|c)Yk;Y!larS@A0IZt=Nb z|6pCPHMlu=N$}?2ZNb}vUk-jXcyI8b;3L8B1|JW8Klp>-^TA&Pe;Ir^_?zJGBw|Tl zNsJ^`qLK8MBuR9V6iJT6ASscImQ0Y`B$+6wlQ<+(B@L2WBzH+xN>)qSBx@z>C67zm zC7UFhB~MGXNcKwJlDsY1FF7bVEIBGUCV5YCLh_;HBgxeec}P}Bb;#V1jUoF(zLW+? z6Qn89G-vF1NlT<=X}Q!Qt(4lNwbDsahjgm6LAp@-p!6~6O6h9pI_U=KM(GpM z?b27I`=ke@howiQ?@K?Bo|K-FekHvcDhdq_4G9emm4(VfBSZU!MukR)szWnFhlCCb z%?`~8%?&k%=7$!B7KN6CnnS0CE)3lqdMNb!u!ykiu<>EHggqSA7Pc;IL)gZ!EnzQ( zy%M$~Y**OsusvZ%!j6T#7xqEe$*@ylzsP#Y0%amuunfpcvIv<%)?cQT4VC4{a%D!D zNmeW?m6gdR$fn9z+3m7BWb{Cfg``LiVKWDcLi!XJyaJw#i`bQ*1=ps@g21X2u$cPvmF*G78VtB;3 zh?^r;MeK?=7a0(#i!?_zL@tb68o503p~y!f*FX>@+^LwaxJ$7}akt`L#S+C*#RH0V#U{mO#nXx{isuwt6)z~ZD_&N-s@SRcNbzf5 zRo~pcllm_1`*h!feJ?2klv1TkDOW}+W0grtoiar^P&r7Mp)@KBlqO|~(yT03HYjH+ zZ&Py0xyl8~h04Xsdz34btCdeGwK~O9rHe|5GDkH;Es1(I>bQ2=H)k4)`)dQ-Ps@1AC)mqhh)#Ivm)h5-8s+U!-s&=V%tM;fqQC(Jjulhmtlj@r4 z_h=kVMoXeYqhq4uqZ6b1M<++8MyExmN9RRbqU)j^(Nm)vqMgyxqFvD~(KDiFMc*8K zU-bRa%c7S@KNS5)^kdO0qgO|_MX!xsAH6&JlNcr@A*LjzIc9mx^D*znd=qmu=330} zYOL<94pE1yWoo%PQr%bGPo1RJsZ-Ty>U6bLeUsX*u2VbIjp`MEtooe#y!vzXMfF$e%j$2`->I*te~j%D8x$*!mBdP8 z!(zi@BVrY?QL)jn>e!sv>e#ul>tf%Ey&M-3ml{_Q*BCb=Zg$))aks|JkGn7K{WHEX_R4qneePR?S+?22H!>NzK!mXEj?jFKP~G4r|`k z9M`e~!PJ zKqiP2WXv~x-q)(x@w(G*QlGObLpn*X6bI$ z-JzSWyGyrN_keD>?qOYjU2%c#znm|@ST&v0fmXS8I@$ao}UbH>h${TatIPGp?S zIGyoH#>I?F8Q)}lpYdbH&za)P*vzcVQJMD4Nttz-j?6nUmt-!>d@%Eo%oUldGPh^$ z&ipv@Z02Xf9vil9*nwfkhW(aRlQl1EY1Z67(YdZWHTU!*V9SL#RVN9)JwC+O?-v-FGg59n9w zpU`j7Kd*m5|C0VS{T}^Z{XYFW`s4c3`Y-jD_225R=zrAzoEw-Mo*R*?%gxLknwy=g z&o$?c&7F{I%dO3=%bk+jkUKkfPA;1}H+Np{UAc>L@6LTFcT?_bxrcI3=Kh-dn<2m; zF@zez4UvWzL!2SO(BF_`$Td_M>I_YW8HU-0IR@5nyJ3xChvBT@iZR3(Zyac>Fjg8z z8b=$)8LN#GjkU%)BWql0Y%^{*?l*pJ{L*;Y_^t7Z@h9Weya9QG^0M>vdB(hgyrR64 zywQ2Jc~kNl@|yBod3WZ`&$}=0sl4a%j^=%wcQ)@_-i3UeACw=QFU^*ZpPN4~e?k6|{0;e=@?Xn;JO6n8iTslV+JcM%LqTCdWx@D@1qF8( zyijnU;I~3?p{_8qu&l7MaBAV=!g~wvFI-mmVBsT$D+*T?wiUiq_-f&bZ!(lpNGFx8tH zP18&do7S5iH$7o`()6Nfujzv6qUn<98`JluA5B+H*NRY)yhu?LRTNVcSCmlHuV`RV zX3@~1?4q2a(xUPrYte|J+M;Pi?xN{Mvx-`aHWxip^jy)_qEkf|i%~II%oO_<_bCoKN@Ge>O0!E1rFo@=rKP21r4^;t(ut)_rL#(JDZQ=q_R>YA>q|G5ZYtehdZ_eh z>G9GNr6)^Im!2v8wDg)8nMpHa_BZ!22bpDNm04}pm=nzd%v$qMbGBJ;Hk!-L4Gu)X$n>)(vF#B8u0TB>!1!PfN07X%8L4;AKSQG}$Oy@Oo$<)+Q zb4>vim(0vEKQ%SCG_w>iZ8r?mhSH=e*DRyw5q;kCGpT zFBRKf(9 z2vsl%ronXZ!ZR=vo`ZR?1bna*mca^G4QpU6Y=lkF09)Zz*a@$}yKoBLgVXRnd;n*_ zdlnku9Gr&_;R0NQOYjkV44=T~@HKo3H{pBukw{4h;fajMiGqZZ7NjLn5jAN;+LA~T zMcR>Q5<_B1GU-gZkglW`=}odpAJUiPk~~sChLGW81Suxt$avx=m1F{`CLXeoyg(L_ z#bk+>_{dVSj4UTFk`-hnd5P4Kda{9RBwNV8$X>FK>?d!L1LPn%N!}%=$a~}r`GVXe zx5yoGpFAMHQlJtlrE;pEp;So)s-XsIqGoERZD?B>Nn>aS+L894nY16xqXXz*T0jfw za9Tu1(^BevnogtBsh1+1L7$;B>9ceeolWP_#q>qGg082vbOU{vZl|x%*XS;~mmZ<- z&=d4MdYYc4jr0P&NI#*U(l6*`dYk^l5sq>kC*h==ifhHS=E68NXC!O5aL&frxwc$9 zm%t@*9l0bfl}qC?xxOv9Jg$%%&W+&4a^rj(B;6&LgVDaLlHR@S5WN|U5DpPw0y9{^ z3O1Bt2=XXHIVvy|TWkggI6;Iq&=w*g3ff^yd<+vX343EU_QU=d?|W8S=u?H1s5(Js zh^~cXRMtWl6j;goz2Z{OK}&cHl+XiG{iP|Ws)aNR!}xQM!LlYI)S?dcXh0)|V+5ME!brB> zBKB7dW1xh+UEn}7wr+_I_UFVHjAifkY!l*lO>8-U6jnn`EqKs^_Qt>}CqvB!coL@g z|Iv!Jz`tFv35MVj5N5EYrd3rHPpJchTv_u(pM}|=sfAf6*1{ZY!%UQCav0!wSO}Ur zm=6oEEk@SC3$O^IupKKNstotG+8i8pIn=E8&%nP)D`C}$ocs)TiN{@8QC!7_SqZsz zX!;4v7lZbA32Oa4uY>g%haKu*5xl_qjl$@`V=K!_t4EbqmeiEHDm?vM72`bPgMDv? zEk#1>oc#VZp5ifOuBbmosfW$}ahf~d2HX9sc^MOH;T7!2raMrKEreZg05n_Sb$A1I z!<(=N{sntsAMA&>uoEU@XY7Jqu^V>B9+-luTj3xa3fSWa9ED?W9NuBpNW-3N`yH5$ z8JLM#*o%Ed`)-JhT!+{M-=Q|?3@m=Ued@PNE9B7LJgG0>OFsfxUM*a~J{TXYconV% zEA|a8@;clY^=OF!OVz`5zpaARZ^5m4sL+o7_c4^V8sk6APz z+A_66VjN3%j*FW%t&y;CiINC@hbb-g6z8hA31cRdxvM>Kapwu;B+?K*+7}v?%zZAj z^1T$VYhkvU?GC5d#vB>d&UYZbRL7cTSNC;IDPVq+ot?paCcCB#1_V&4)!SlX+sAc?Pe|<8t*7sHLV8+qoTW%`Bz8>hm>eJ1GbSrJDJ~{4 zE3tD-a$Ht=OlCsojHJv?NuA=-dJZni$?uX}Bq$5quF3hH$|_fE^Jm*AxWK* zJ9p{oo6)hgP!M$I_3KgI*u6(es?Qc}3r$IT98Y@AM%t4Eh^{AbZ0+$l21~HCo+Pr*By_RQiLBb>PI*zJ8%cxc z4Wv8iK~hL6j>T~}9^D&APm<2|tS3H>Phc5)#{2Gd8ml1vNPo~UD@6O&B^&k4a~?qQ znM0F-SYAs8VMUSN|KM zZ6n+J9wc|MYz}GJDoht)bu{uad5b|4iaq0pxhqOtPxf|Kcx>Uj6sW-xPqgYOShOG^#b)K!MUjN$&~zL}k^(ngcn z5~!h`J8xfAgOAm)+3OTWfi^PF2$ZDw+b)@wkalG5yu)WID(a%5>)N$@M4WXovG&x7 zL5tf{7q$(gYU5Z(9TGPfjPVJ+(W2h_HImkf8!fY&)S$$a$8U~}7%zA=JsFh4U zt!JEgmiLiabur{Mk=nO)V+MNi6$T_ed|97@)#O+tQ4c8ZrZ-w=qwg zOr9iD$Wvr0d73R^I`I<3Dfkpl#iwx^PDd{y&cJ7I<~H&Sl#pkc>&|A*JD0idJeHo1 z&$4hk8|UC$d=Agy6}->V4=}z+u;jbSeDx_2zI{Emhqcbm$joq!Ev_l^WVpuEj7zI> z75kn_vB!<-U;L!I-2IfRG@pUdRV1ha<>Opc`HV3=em*)!R*}_=QP;p4vex%{ew!$P z;H=<2tt0E19Le^>F^>VWj?_Z#pEG!dA5!Z$pvzeA>$*+V(9pkiK`Sy z_SL7q5{WP4Hb(3(;&%2_q4EN`Oui&nSeviN*W@bshFl}xlIu|7!wh!|+=wf&0k`4` zjP>2m=rojk$CM3&+HLY5@&m5ImvBASv$#ioB0sY}ej#_sy~6BL=JxKfZdcXdz_#IP zT!U->$ti+6{2Te*wQI=0%i%&!dXgQL(8%CqcRti!cK z@+)hqN?g?fZEn*d&}KuR&BW};4paq7+6sEpFvuF-tY(-{9OiMCGeRjYpMb4!6EeqR zvWaS`&bK&AE1`NemBm>aV z!!rs7Ww{ys`B6e`3?+Zc2zKwFPT$C0(Gn`M?ich*cl{+AdV4Y~iLEFeZ`ECV8Kk2{z+q48|;2{h67e1D@CU&XWk2oai0JCjdo zS3g7o=-7`t89L7V4IMOvrZ%Gk_u*^JzzB|;#ehM3(cUzh_Q75F2EK{^dISvGzr7zB z`1(Hsq1ko=1Gd}UZ10#Rdk+cNd(WTk-P~eCpvB&1YkU7}?Vw$o+>yHI1a<_VW9c|L zp1SGdEYO$Ha#}$v@hv=n2k{WTjfe3F9>rsLd@G$8JOa>~;1PgM0SCSl&}I%i*>nPU zpM5hLG6X@GURk=UyE&7j%waxB=i-T`(*S+`A5R0#6I();vY1MJ_--v-hNoCe%`-W@ zE9n})E?+=bV95~Otj<=@=nM!Gqm>1hMg^dJ21wnc*8UE%h$ zE$UL`DzBX6%BW#{Si&f>x=WkLAQE@bSBnJoUy;aowB(68x|4y}H=v#O&lEymr*8y7 zLV3K5=bJ!M7X-;ZsM$pK)3@jWdXOHXZ{vq}5kJCD@H71U53qEl#~4=5;00VOgfv_F zWWdsw{FWZ}NJ0h*6W9j4AAr}#|3AFW1-kh3ukgAQfY-nO30|KCT71z2ueRQgsfvC{ zuh6gP*KE+MEZTlUuR(9}fL>=;AZzFidK0qfcl3MaqKsRrJd7>UE6ZxiD{^Ye$GEDB zgjTMK(!XV@O2!vg6_9mde(nm_qyCzQ z`E%x9`>QO$Rsx4#3R(mqto6gDU(m$=p+6J}T1HiaTu*v3nvZ);gV)m^=^a1g1S+)t zhZ=sCY(DVOpXmc8GU+e$F1<(Z<5&1KUd3-V&|m3q^mqCYui-8H9&cm((0;D5fyg^- zpnKf-K&r4`@s!FMPyfnNmrs?d>(GQ|E`$qZeR4b}lzI439Kk9hY{K<6U47|_&nQCvGN8t>pw_;Wq|1sq%)TW3rBg*oo1 zv{%lgsCEmkm)&l>lMNcjrqM7z)cO!vAI+w-uel*Q& z{I%)G%JuV4vp<)Ezs(@fG*1U8xdGhZzmCWiun~vi?`*_}Y@%KA4GwN3SNykXW1xsD zok0jXW)R9MPR%!ZnR3;G4Wij~VHXx82C(as5ip9eXa$|hZn75AWpou?LtmoxbR%t` zTbN^iL+`M=DV`ZDlxxW;*)3ITc0(nyo2e)+nv3P)*bP-8yPZnra=9{Y3b%->;|_9X zxtrWA?mqV$_fSG55=n?8Ns=z{W=VQW`bzpsawQ`qrIHDfDv3ujNlK-mQjOFsbxNb8 z3DQ*QVCfL)FzImVNNJI@SXv@=Nykau(&I^a zdM88^(j{bc$g?5!A%{ag54jd{Bji@d_aS$A!gIWo=Xp6F%D43LN?ylXcqcFN349Wt z%y;Fx^C|oY-pyC=6Zk5AGCzf%%1`6x^DFq({04p_-@w1hzr`QsPw}VukNMB}%lsAo z8h@R?$$!V+k#VvXGMg+?mL%&e>niIZOO^GM<;mQ#NwVi<3uG_I7R!9HWwIA#D`l%? zYh~Vbve#w1WqV|MW&33ZWQSyjWk+SlWhZ3s%FfGf$bOb9dRX=r+=D|ANa zn$R~x4~3pkc2o9H_EHX3j!>2;Cn&wj8OoW;S;{%e=akPY7bw>#UsA4D)+skAHz_wO zw<=#&9#l3euPW~eEd`4ZD|8il3K>F{&|AnA1_=4WU}1>xl(0}(FKidQyM;Z%USYp* zKzK(uDV!2c3m*t)g=@mEDzz$3)m_y?m7+>hjZnE&WvU9*L{+t_Mzu(_Qng#PN3}Ps zB5ZQl`mlzuGwP1&p6X0>FLfVvKXs0JlG>}DuYN(jSiMxeT)jfQUwuq{Tzx`)R{d}F zPwHRP_tX#6zo{Q;h*!gDq#9l$*Mw?p8iz*IwADmuqBXIaI8D4JQIn)e)^yQ~)XdYY z(KKk@)qJUyX-(QRZJxGNJ5f7JyF$B4yGFZFyHmScdsur~`+@c=?JezX?GM^JI-+|_ zC+J$~!gLy)PG``y(M9Uo>0)&4bscmGx{kVRU6IaPuA8D;savhvtlOd6soSM{L$_ad zKzB%YSa($Sk?yAMcRjCH=v(L?)3?|6)mQ3g=-26Y=@05Z(|@kNtiPiFTK|pyTm23F zEklSwZ)j^sHRKyA4HFI3h8n|U!xY0*!*;`N!$HH_h9ib!hIb4n4HpcT4c{2PHQX@V zGCcGeiBVyUG{zeH8w-sijH8UBjZ=)XjdP9jjPs34jmwSo#*M}X;}+vtG2(8-{fJ*9em8;1OHC3}h)HI$n(QX0sf{Vp)Xo%RN;0LG z(oE^5Oj9pYwyD50)Kq91VX81qHBB>lO*2fZObwltKI6fwy{QAW36%4cx#%qkF~G0zqQa>WG%LqSjSr*x0YEetkbM3t##JD)`QlI z)^BapCb5OsWHyDZh0XhzO>1-5L|a>16uS(IwZ+-uZ7H@iTe>aN*2^}?R$v=yE3}o` zCfcfPHMYsN=WNSuD{QN5Yiw`W4%?2}j@wSy&e%S)eQvvKyJ8QuC))ekN83I2Irdfd z9rh#kWA=CKC+(-~r|louFWE2KA2_HZ+!60carAcdarE;#avXV%VUFRBk&Yrqv7^LM z=9uJ|;aKU|Dc9X!||r$kmIo9sN=ZfgyUVuHK)|s)|uwaaSm|iI}4mcorTT{ z=Tzr(Cpu?3XF2CMS35U2w>Y;sw>x(@_dAa`k2&9Qo^+mZHagEcFE}qbuR4DeLqw$* zF51Ou>}p1g6ywBrF;Pqslf^VKN6Z!T#DQXwI7TcL$BAz73DF}?5vPjNMHCl^OU32l z3UQTKE7pq}#RhSUxJ%qGo)piBjpBLng7}&Eh4`iTwfK#AFL)_JY4FXB55Ak9ZRr02 Dh)hpg diff --git a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata index 1eef849..ca3329e 100644 --- a/ComposableArchitecture.xcworkspace/contents.xcworkspacedata +++ b/ComposableArchitecture.xcworkspace/contents.xcworkspacedata @@ -4,10 +4,4 @@ - - - - diff --git a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved index cf43ed3..80d20a1 100644 --- a/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/ComposableArchitecture.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,24 +1,6 @@ { "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", @@ -29,21 +11,12 @@ } }, { - "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", + "package": "ReactiveSwift", + "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", "state": { "branch": null, - "revision": "579364393ad587352bcce133b7b3f73392cb585b", - "version": "11.2.2" + "revision": "efb2f0a6f6c8739cce8fb14148a5bd3c83f2f91d", + "version": "7.0.0" } }, { @@ -100,15 +73,6 @@ "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", diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/IDEFindNavigatorScopes.plist similarity index 72% rename from Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/IDEFindNavigatorScopes.plist index 18d9810..5dd5da8 100644 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/IDEFindNavigatorScopes.plist @@ -1,8 +1,5 @@ - - IDEDidComputeMac32BitWarning - - + diff --git a/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate b/ComposableArchitecture.xcworkspace/xcuserdata/nguyenphong.xcuserdatad/UserInterfaceState.xcuserstate index 793131d51d418e0f67b3abe3d12957b14b0de934..561a2780d48415cd05cfdf5a441714d1551c3285 100644 GIT binary patch literal 367307 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 literal 272511 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 diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj b/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj deleted file mode 100644 index 19e818e..0000000 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.pbxproj +++ /dev/null @@ -1,772 +0,0 @@ -// !$*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 deleted file mode 100644 index 919434a..0000000 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(SwiftUI).xcscheme b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(SwiftUI).xcscheme deleted file mode 100644 index 4159c51..0000000 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(SwiftUI).xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(UIKit).xcscheme b/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(UIKit).xcscheme deleted file mode 100644 index 770d9fa..0000000 --- a/Examples/CaseStudies/CaseStudies.xcodeproj/xcshareddata/xcschemes/CaseStudies(UIKit).xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 186b90e3fe4c070ff0bbef31ad85baa47a055d38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8152 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 YhpMf=!oN%>5_=LoEhEiJb^D0_0Whjby#N3J diff --git a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index e4b96da..0000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "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 deleted file mode 100644 index 73c0059..0000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift b/Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift deleted file mode 100644 index 35abb2c..0000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/ContentView.swift +++ /dev/null @@ -1,14 +0,0 @@ -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 deleted file mode 100644 index 73c0059..0000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift b/Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift deleted file mode 100644 index 1cfeafc..0000000 --- a/Examples/CaseStudies/SwiftUICaseStudies/SwiftUICaseStudiesApp.swift +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 26cbdef..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/AppDelegate.swift +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index eb87897..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index 186b90e3fe4c070ff0bbef31ad85baa47a055d38..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8152 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 YhpMf=!oN%>5_=LoEhEiJb^D0_0Whjby#N3J diff --git a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index e4b96da..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,99 +0,0 @@ -{ - "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 deleted file mode 100644 index 73c0059..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift deleted file mode 100644 index 15ae7d4..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthAction.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index a96bc7e..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct AuthEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift deleted file mode 100644 index 8106c4f..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthReducer.swift +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 0f0ea3d..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthState.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct AuthState: Equatable { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift deleted file mode 100644 index d4a2a00..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/AuthScreen/AuthViewController.swift +++ /dev/null @@ -1,117 +0,0 @@ -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 deleted file mode 100644 index 865e932..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,25 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard b/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard deleted file mode 100644 index d267725..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Base.lproj/Main.storyboard +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift deleted file mode 100644 index 2c85ba0..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterAction.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index e756835..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct CounterEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift deleted file mode 100644 index 23841d5..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterReducer.swift +++ /dev/null @@ -1,24 +0,0 @@ -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 deleted file mode 100644 index a12eea9..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterState.swift +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index d00298a..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CounterScreen/CounterViewController.swift +++ /dev/null @@ -1,129 +0,0 @@ -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 deleted file mode 100644 index 402ae30..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableAction.swift +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 6d14050..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct CountersTableEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift deleted file mode 100644 index b7a5429..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableReducer.swift +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 5fef0b9..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableState.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 6117091..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/CountersListScreen/CountersTableViewController.swift +++ /dev/null @@ -1,133 +0,0 @@ -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 deleted file mode 100644 index df66182..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationAction.swift +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 53bbd5c..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct EagerNavigationEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift deleted file mode 100644 index 87f544c..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationReducer.swift +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index 7b1148e..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationState.swift +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index 25461b2..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/EagerNavigation/EagerNavigationViewController.swift +++ /dev/null @@ -1,144 +0,0 @@ -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 deleted file mode 100644 index dd3c9af..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Info.plist +++ /dev/null @@ -1,25 +0,0 @@ - - - - - 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 deleted file mode 100644 index 940151f..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Internal/ActivityIndicatorViewController.swift +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index 9fe1209..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/Internal/IfLetStoreController.swift +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 283186a..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationAction.swift +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index 69c27f4..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct LazyNavigationEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift deleted file mode 100644 index 6d84ee6..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationReducer.swift +++ /dev/null @@ -1,38 +0,0 @@ -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 deleted file mode 100644 index b93e820..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationState.swift +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index ef23bb5..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/LazyNavigation/LazyNavigationViewController.swift +++ /dev/null @@ -1,146 +0,0 @@ -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 deleted file mode 100644 index 1d33db4..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainAction.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 19d428c..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct MainEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift deleted file mode 100644 index 78a3864..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainReducer.swift +++ /dev/null @@ -1,22 +0,0 @@ -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 deleted file mode 100644 index 11846cf..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainState.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct MainState: Equatable { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift b/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift deleted file mode 100644 index 47889ad..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/MainScreen/MainViewController.swift +++ /dev/null @@ -1,166 +0,0 @@ -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 deleted file mode 100644 index 675506b..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootAction.swift +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 6b791ec..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct RootEnvironment { - -} diff --git a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift b/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift deleted file mode 100644 index be85597..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootReducer.swift +++ /dev/null @@ -1,29 +0,0 @@ -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 deleted file mode 100644 index b0b3276..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootState.swift +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index dc7e0a0..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/RootScreen/RootViewController.swift +++ /dev/null @@ -1,126 +0,0 @@ -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 deleted file mode 100644 index 0fa5fab..0000000 --- a/Examples/CaseStudies/UIKitCaseStudies/SceneDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -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/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index eb87897..0000000 --- a/Examples/Todos/Shared/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "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 deleted file mode 100644 index c136eaf..0000000 --- a/Examples/Todos/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "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 deleted file mode 100644 index 73c0059..0000000 --- a/Examples/Todos/Shared/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift deleted file mode 100644 index 1c13ae7..0000000 --- a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthAction.swift +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 433425c..0000000 --- a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct AuthEnvironment { - -} diff --git a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift b/Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift deleted file mode 100644 index 8a7a1c2..0000000 --- a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthReducer.swift +++ /dev/null @@ -1,20 +0,0 @@ -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 deleted file mode 100644 index fe78e09..0000000 --- a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthState.swift +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 356d855..0000000 --- a/Examples/Todos/Shared/TodoApp/AuthScreen/AuthView.swift +++ /dev/null @@ -1,89 +0,0 @@ -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 deleted file mode 100644 index 333a112..0000000 --- a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterAction.swift +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 824cb72..0000000 --- a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct CounterEnvironment { - -} diff --git a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift b/Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift deleted file mode 100644 index 11d83fe..0000000 --- a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterReducer.swift +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 3010941..0000000 --- a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterState.swift +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index 9021b3b..0000000 --- a/Examples/Todos/Shared/TodoApp/CounterScreen/CounterView.swift +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index 1042dfc..0000000 --- a/Examples/Todos/Shared/TodoApp/MainScreen/MainAction.swift +++ /dev/null @@ -1,21 +0,0 @@ -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 deleted file mode 100644 index 19d428c..0000000 --- a/Examples/Todos/Shared/TodoApp/MainScreen/MainEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct MainEnvironment { - -} diff --git a/Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift b/Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift deleted file mode 100644 index f8f395f..0000000 --- a/Examples/Todos/Shared/TodoApp/MainScreen/MainReducer.swift +++ /dev/null @@ -1,99 +0,0 @@ -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 deleted file mode 100644 index 07282d0..0000000 --- a/Examples/Todos/Shared/TodoApp/MainScreen/MainState.swift +++ /dev/null @@ -1,9 +0,0 @@ -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 deleted file mode 100644 index b6cf5a3..0000000 --- a/Examples/Todos/Shared/TodoApp/MainScreen/MainView.swift +++ /dev/null @@ -1,193 +0,0 @@ -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 deleted file mode 100644 index db47fe3..0000000 --- a/Examples/Todos/Shared/TodoApp/Models/Todo.swift +++ /dev/null @@ -1,7 +0,0 @@ -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 deleted file mode 100644 index e8f194e..0000000 --- a/Examples/Todos/Shared/TodoApp/RootScreen/RootAction.swift +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 6b791ec..0000000 --- a/Examples/Todos/Shared/TodoApp/RootScreen/RootEnvironment.swift +++ /dev/null @@ -1,6 +0,0 @@ -import ComposableArchitecture -import Foundation - -struct RootEnvironment { - -} diff --git a/Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift b/Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift deleted file mode 100644 index ed56b31..0000000 --- a/Examples/Todos/Shared/TodoApp/RootScreen/RootReducer.swift +++ /dev/null @@ -1,27 +0,0 @@ -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 deleted file mode 100644 index b0b3276..0000000 --- a/Examples/Todos/Shared/TodoApp/RootScreen/RootState.swift +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 4b9acf3..0000000 --- a/Examples/Todos/Shared/TodoApp/RootScreen/RootView.swift +++ /dev/null @@ -1,90 +0,0 @@ -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 deleted file mode 100644 index 313f571..0000000 --- a/Examples/Todos/Shared/TodoApp/View+/View+.swift +++ /dev/null @@ -1,9 +0,0 @@ -import SwiftUI - -extension View { - func embedNavigationView() -> some View { - NavigationView { - self - } - } -} diff --git a/Examples/Todos/Shared/TodosApp.swift b/Examples/Todos/Shared/TodosApp.swift deleted file mode 100644 index 117c539..0000000 --- a/Examples/Todos/Shared/TodosApp.swift +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index 926855a..0000000 --- a/Examples/Todos/Todos.xcodeproj/project.pbxproj +++ /dev/null @@ -1,750 +0,0 @@ -// !$*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 deleted file mode 100644 index 919434a..0000000 --- a/Examples/Todos/Todos.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d9810..0000000 --- a/Examples/Todos/Todos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme b/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme deleted file mode 100644 index 80a7221..0000000 --- a/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (iOS).xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme b/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme deleted file mode 100644 index caa2a17..0000000 --- a/Examples/Todos/Todos.xcodeproj/xcshareddata/xcschemes/Todos (macOS).xcscheme +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Examples/Todos/macOS/macOS.entitlements b/Examples/Todos/macOS/macOS.entitlements deleted file mode 100644 index 40b639e..0000000 --- a/Examples/Todos/macOS/macOS.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - 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 index 4b1173d..023ccb7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -15,8 +15,8 @@ "repositoryURL": "https://github.com/ReactiveCocoa/ReactiveSwift.git", "state": { "branch": null, - "revision": "c43bae3dac73fdd3cb906bd5a1914686ca71ed3c", - "version": "6.7.0" + "revision": "efb2f0a6f6c8739cce8fb14148a5bd3c83f2f91d", + "version": "7.0.0" } }, { @@ -42,8 +42,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-case-paths", "state": { "branch": null, - "revision": "d226d167bd4a68b51e352af5655c92bce8ee0463", - "version": "0.7.0" + "revision": "241301b67d8551c26d8f09bd2c0e52cc49f18007", + "version": "0.8.0" } }, { @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/apple/swift-collections", "state": { "branch": null, - "revision": "2d33a0ea89c961dcb2b3da2157963d9c0370347e", - "version": "1.0.1" + "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", + "version": "1.0.2" } }, { @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-custom-dump", "state": { "branch": null, - "revision": "c2dd2c64b753dda592f5619303e02f741cd3e862", - "version": "0.2.0" + "revision": "51698ece74ecf31959d3fa81733f0a5363ef1b4e", + "version": "0.3.0" } }, { @@ -69,8 +69,8 @@ "repositoryURL": "https://github.com/pointfreeco/swift-identified-collections", "state": { "branch": null, - "revision": "c8e6a40209650ab619853cd4ce89a0aa51792754", - "version": "0.3.0" + "revision": "680bf440178a78a627b1c2c64c0855f6523ad5b9", + "version": "0.3.2" } }, { diff --git a/Package.swift b/Package.swift index ecff37f..7993328 100644 --- a/Package.swift +++ b/Package.swift @@ -17,7 +17,7 @@ let package = Package( targets: ["ComposableArchitecture"]), ], dependencies: [ - .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "6.7.0"), + .package(url: "https://github.com/ReactiveCocoa/ReactiveSwift.git", from: "7.0.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"), diff --git a/README.md b/README.md index 23b06b4..36219be 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,6 @@ https://github.com/FullStack-Swift/rxswift-composable-architecture https://github.com/FullStack-Swift/reactiveswift-composable-architecture -## Example +## Examples -https://github.com/FullStack-Swift/TodoFullStackSwift +https://github.com/FullStack-Swift/TodoList diff --git a/Sources/ComposableArchitecture/Beta/Concurrency.swift b/Sources/ComposableArchitecture/Beta/Concurrency.swift deleted file mode 100644 index 178b5cd..0000000 --- a/Sources/ComposableArchitecture/Beta/Concurrency.swift +++ /dev/null @@ -1,103 +0,0 @@ -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/Combine+ReactiveSwift/AnyDisposable.swift b/Sources/ComposableArchitecture/Combine+ReactiveSwift/AnyDisposable.swift new file mode 100644 index 0000000..ce0fe2d --- /dev/null +++ b/Sources/ComposableArchitecture/Combine+ReactiveSwift/AnyDisposable.swift @@ -0,0 +1,12 @@ +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)) + } +} diff --git a/Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift b/Sources/ComposableArchitecture/Combine+ReactiveSwift/Combine+ReactiveSwift.swift similarity index 73% rename from Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift rename to Sources/ComposableArchitecture/Combine+ReactiveSwift/Combine+ReactiveSwift.swift index 6e9d68d..1806816 100644 --- a/Sources/ComposableArchitecture/Beta/Combine+ReactiveSwift.swift +++ b/Sources/ComposableArchitecture/Combine+ReactiveSwift/Combine+ReactiveSwift.swift @@ -1,9 +1,9 @@ -#if canImport(Combine) import Combine import ReactiveSwift -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) public extension Publisher { + /// Convert the publisher to an Effect + /// - Returns: Effect func eraseToEffect() -> Effect { SignalProducer { observer, disposable in let cancellable = self.sink( @@ -25,4 +25,10 @@ public extension Publisher { } } } -#endif + +//extension SignalProducer { + /// Convert the observable to an AnyPublisher +// var publisher: AnyPublisher { +// fatalError() +// } +//} diff --git a/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift b/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift index 033f0b3..aa17beb 100644 --- a/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift +++ b/Sources/ComposableArchitecture/Debugging/ReducerDebugging.swift @@ -1,12 +1,45 @@ import CasePaths import Dispatch +/// Determines how the string description of an action should be printed when using the +/// ``Reducer/debug(_:state:action:actionFormat:environment:)`` higher-order reducer. public enum ActionFormat { + /// Prints the action in a single line by only specifying the labels of the associated values: + /// + /// ```swift + /// Action.screenA(.row(index:, action: .textChanged(query:))) + /// ``` + /// case labelsOnly + /// Prints the action in a multiline, pretty-printed format, including all the labels of + /// any associated values, as well as the data held in the associated values: + /// + /// ```swift + /// Action.screenA( + /// ScreenA.row( + /// index: 1, + /// action: RowAction.textChanged( + /// query: "Hi" + /// ) + /// ) + /// ) + /// ``` + /// case prettyPrint } extension Reducer { + /// Prints debug messages describing all received actions and state mutations. + /// + /// Printing is only done in debug (`#if DEBUG`) builds. + /// + /// - Parameters: + /// - prefix: A string with which to prefix all debug messages. + /// - toDebugEnvironment: A function that transforms an environment into a debug environment by + /// describing a print function and a queue to print from. Defaults to a function that ignores + /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print` + /// function and a background queue. + /// - Returns: A reducer that prints debug messages for all received actions. public func debug( _ prefix: String = "", actionFormat: ActionFormat = .prettyPrint, @@ -22,7 +55,18 @@ extension Reducer { environment: toDebugEnvironment ) } - + + /// Prints debug messages describing all received actions. + /// + /// Printing is only done in debug (`#if DEBUG`) builds. + /// + /// - Parameters: + /// - prefix: A string with which to prefix all debug messages. + /// - toDebugEnvironment: A function that transforms an environment into a debug environment by + /// describing a print function and a queue to print from. Defaults to a function that ignores + /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print` + /// function and a background queue. + /// - Returns: A reducer that prints debug messages for all received actions. public func debugActions( _ prefix: String = "", actionFormat: ActionFormat = .prettyPrint, @@ -38,7 +82,20 @@ extension Reducer { environment: toDebugEnvironment ) } - + + /// Prints debug messages describing all received local actions and local state mutations. + /// + /// Printing is only done in debug (`#if DEBUG`) builds. + /// + /// - Parameters: + /// - prefix: A string with which to prefix all debug messages. + /// - toLocalState: A function that filters state to be printed. + /// - toLocalAction: A case path that filters actions that are printed. + /// - toDebugEnvironment: A function that transforms an environment into a debug environment by + /// describing a print function and a queue to print from. Defaults to a function that ignores + /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print` + /// function and a background queue. + /// - Returns: A reducer that prints debug messages for all received actions. public func debug( _ prefix: String = "", state toLocalState: @escaping (State) -> LocalState, @@ -48,48 +105,49 @@ extension Reducer { 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)) + #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 .merge( + .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) + """ + ) } - 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 + }, + effects + ) + } + #else + return self + #endif } } +/// An environment for debug-printing reducers. public struct DebugEnvironment { public var printer: (String) -> Void public var queue: DispatchQueue - + public init( printer: @escaping (String) -> Void = { print($0) }, queue: DispatchQueue @@ -97,7 +155,7 @@ public struct DebugEnvironment { self.printer = printer self.queue = queue } - + public init( printer: @escaping (String) -> Void = { print($0) } ) { @@ -106,6 +164,7 @@ public struct DebugEnvironment { } private let _queue = DispatchQueue( - label: "ComposableArchitecture.DebugEnvironment", + label: "co.pointfree.ComposableArchitecture.DebugEnvironment", qos: .background ) + diff --git a/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift b/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift index 5c73b71..22b9935 100644 --- a/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift +++ b/Sources/ComposableArchitecture/Debugging/ReducerInstrumentation.swift @@ -1,8 +1,26 @@ -#if canImport(os) +import Combine import os.signpost extension Reducer { - @available(iOS 12.0, *) + /// Instruments the reducer with + /// [signposts](https://developer.apple.com/documentation/os/logging/recording_performance_data). + /// Each invocation of the reducer will be measured by an interval, and the lifecycle of its + /// effects will be measured with interval and event signposts. + /// + /// To use, build your app for Instruments (⌘I), create a blank instrument, and then use the "+" + /// icon at top right to add the signpost instrument. Start recording your app (red button at top + /// left) and then you should see timing information for every action sent to the store and every + /// effect executed. + /// + /// Effect instrumentation can be particularly useful for inspecting the lifecycle of long-living + /// effects. For example, if you start an effect (e.g. a location manager) in `onAppear` and + /// forget to tear down the effect in `onDisappear`, it will clearly show in Instruments that the + /// effect never completed. + /// + /// - Parameters: + /// - prefix: A string to print at the beginning of the formatted message for the signpost. + /// - log: An `OSLog` to use for signposts. + /// - Returns: A reducer that has been enhanced with instrumentation. public func signpost( _ prefix: String = "", log: OSLog = OSLog( @@ -11,12 +29,12 @@ extension Reducer { ) ) -> 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 { @@ -26,8 +44,10 @@ extension Reducer { let effects = self.run(&state, action, environment) if log.signpostsEnabled { os_signpost(.end, log: log, name: "Action") - return effects + return + effects .effectSignpost(prefix, log: log, actionOutput: actionOutput) + .eraseToEffect() } return effects } @@ -35,14 +55,13 @@ extension Reducer { } extension Effect where Error == Never { - @available(iOS 12.0, *) func effectSignpost( _ prefix: String, log: OSLog, actionOutput: String - ) -> Effect { + ) -> Self { let sid = OSSignpostID(log: log) - + return self.on( starting: { os_signpost( @@ -63,7 +82,6 @@ extension Effect where Error == Never { }) } } -#endif func debugCaseOutput(_ value: Any) -> String { func debugCaseOutputHelp(_ value: Any) -> String { @@ -79,17 +97,19 @@ func debugCaseOutput(_ value: Any) -> String { case .tuple: return mirror.children.map { label, value in let childOutput = debugCaseOutputHelp(value) - return "\(label.map { isUnlabeledArgument($0) ? "_:" : "\($0):" } ?? "")\(childOutput.isEmpty ? "" : " \(childOutput)")" + 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 index 752a337..60369bf 100644 --- a/Sources/ComposableArchitecture/Effect.swift +++ b/Sources/ComposableArchitecture/Effect.swift @@ -1,24 +1,167 @@ import Foundation import ReactiveSwift - + /// The ``Effect`` type encapsulates a unit of work that can be run in the outside world, and can + /// feed data back to the ``Store``. It is the perfect place to do side effects, such as network + /// requests, saving/loading from disk, creating timers, interacting with dependencies, and more. + /// + /// Effects are returned from reducers so that the ``Store`` can perform the effects after the + /// reducer is done running. It is important to note that ``Store`` is not thread safe, and so all + /// effects must receive values on the same thread, **and** if the store is being used to drive UI + /// then it must receive values on the main thread. + /// + /// An effect simply wraps a `Publisher` value and provides some convenience initializers for + /// constructing some common types of effects. public typealias Effect = SignalProducer extension Effect { - public static var none: Effect { + + /// An effect that does nothing and completes immediately. Useful for situations where you must + /// return an effect, but you don't need to do anything. + public static var none: Self { .empty } - public static func fireAndForget(_ work: @escaping () -> Void) -> Effect { - .deferred { () -> SignalProducer in - work() - return .empty + /// Creates an effect that can supply a single value asynchronously in the future. + /// + /// This can be helpful for converting APIs that are callback-based into ones that deal with + /// ``Effect``s. + /// + /// For example, to create an effect that delivers an integer after waiting a second: + /// + /// ```swift + /// Effect.future { callback in + /// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + /// callback(.success(42)) + /// } + /// } + /// ``` + /// + /// Note that you can only deliver a single value to the `callback`. If you send more they will be + /// discarded: + /// + /// ```swift + /// Effect.future { callback in + /// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { + /// callback(.success(42)) + /// callback(.success(1729)) // Will not be emitted by the effect + /// } + /// } + /// ``` + /// + /// If you need to deliver more than one value to the effect, you should use the ``Effect`` + /// initializer that accepts a ``Subscriber`` value. + /// + /// - Parameter attemptToFulfill: A closure that takes a `callback` as an argument which can be + /// used to feed it `Result` values. + 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) + } + } } } + /// Initializes an effect that lazily executes some work in the real world and synchronously sends + /// that data back into the store. + /// + /// For example, to load a user from some JSON on the disk, one can wrap that work in an effect: + /// + /// ```swift + /// Effect.result { + /// let fileUrl = URL( + /// fileURLWithPath: NSSearchPathForDirectoriesInDomains( + /// .documentDirectory, .userDomainMask, true + /// )[0] + /// ) + /// .appendingPathComponent("user.json") + /// + /// let result = Result { + /// let data = try Data(contentsOf: fileUrl) + /// return try JSONDecoder().decode(User.self, from: $0) + /// } + /// + /// return result + /// } + /// ``` + /// + /// - Parameter attemptToFulfill: A closure encapsulating some work to execute in the real world. + /// - Returns: An effect. + public static func result(_ attemptToFulfill: @escaping () -> Result) -> Self { + return self.init(result: attemptToFulfill()) + } + /// Initializes an effect from a callback that can send as many values as it wants, and can send + /// a completion. + /// + /// This initializer is useful for bridging callback APIs, delegate APIs, and manager APIs to the + /// ``Effect`` type. One can wrap those APIs in an Effect so that its events are sent through the + /// effect, which allows the reducer to handle them. + /// + /// For example, one can create an effect to ask for access to `MPMediaLibrary`. It can start by + /// sending the current status immediately, and then if the current status is `notDetermined` it + /// can request authorization, and once a status is received it can send that back to the effect: + /// + /// ```swift + /// Effect.run { subscriber in + /// subscriber.send(MPMediaLibrary.authorizationStatus()) + /// + /// guard MPMediaLibrary.authorizationStatus() == .notDetermined else { + /// subscriber.send(completion: .finished) + /// return AnyCancellable {} + /// } + /// + /// MPMediaLibrary.requestAuthorization { status in + /// subscriber.send(status) + /// subscriber.send(completion: .finished) + /// } + /// return AnyCancellable { + /// // Typically clean up resources that were created here, but this effect doesn't + /// // have any. + /// } + /// } + /// ``` + /// + /// - Parameter work: A closure that accepts a ``Subscriber`` value and returns a cancellable. + /// When the ``Effect`` is completed, the cancellable will be used to clean up any resources + /// created when the effect was started. + public static func run( + ) -> Self { + fatalError() + } + + /// Concatenates a variadic list of effects together into a single effect, which runs the effects + /// one after the other. + /// + /// - Warning: Combine's `Publishers.Concatenate` operator, which this function uses, can leak + /// when its suffix is a `Publishers.MergeMany` operator, which is used throughout the + /// Composable Architecture in functions like ``Reducer/combine(_:)-1ern2``. + /// + /// Feedback filed: + /// + /// - Parameter effects: A variadic list of effects. + /// - Returns: A new effect public static func concatenate(_ effects: Effect...) -> Effect { .concatenate(effects) } + /// Concatenates a collection of effects together into a single effect, which runs the effects one + /// after the other. + /// + /// - Warning: Combine's `Publishers.Concatenate` operator, which this function uses, can leak + /// when its suffix is a `Publishers.MergeMany` operator, which is used throughout the + /// Composable Architecture in functions like ``Reducer/combine(_:)-1ern2``. + /// + /// Feedback filed: + /// + /// - Parameter effects: A collection of effects. + /// - Returns: A new effect public static func concatenate( _ effects: C ) -> Effect where C.Element == Effect { @@ -30,6 +173,37 @@ extension Effect { } } + /// Merges a variadic list of effects together into a single effect, which runs the effects at the + /// same time. + /// + /// - Parameter effects: A list of effects. + /// - Returns: A new effect + public static func merge( + _ effects: Effect... + ) -> Effect { + .merge(effects) + } + + /// Merges a sequence of effects together into a single effect, which runs the effects at the same + /// time. + /// + /// - Parameter effects: A sequence of effects. + /// - Returns: A new effect + public static func merge(_ effects: S) -> SignalProducer where S.Element == Effect { + return SignalProducer(effects).flatten(.merge) + } + + /// Creates an effect that executes some work in the real world that doesn't need to feed data + /// back into the store. + /// + /// - Parameter work: A closure encapsulating some work to execute in the real world. + /// - Returns: An effect. + public static func fireAndForget(_ work: @escaping () -> Void) -> Effect { + .deferred { () -> SignalProducer in + work() + return .empty + } + } public static func deferred(_ createProducer: @escaping () -> SignalProducer) -> SignalProducer { @@ -37,27 +211,96 @@ extension Effect { .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) - } - } - } + /// Transforms all elements from the upstream effect with a provided closure. + /// + /// - Parameter transform: A closure that transforms the upstream effect's output to a new output. + /// - Returns: A publisher that uses the provided closure to map elements from the upstream effect + /// to new elements that it then publishes. + //MARK: already in SignalProducer + // public func map(_ transform: @escaping (Value) -> T) -> Effect { + // return self.map(transform) + // } +} + +extension Effect where Error == Swift.Error { + /// Initializes an effect that lazily executes some work in the real world and synchronously sends + /// that data back into the store. + /// + /// For example, to load a user from some JSON on the disk, one can wrap that work in an effect: + /// + /// ```swift + /// Effect.catching { + /// let fileUrl = URL( + /// fileURLWithPath: NSSearchPathForDirectoriesInDomains( + /// .documentDirectory, .userDomainMask, true + /// )[0] + /// ) + /// .appendingPathComponent("user.json") + /// + /// let data = try Data(contentsOf: fileUrl) + /// return try JSONDecoder().decode(User.self, from: $0) + /// } + /// ``` + /// + /// - Parameter work: A closure encapsulating some work to execute in the real world. + /// - Returns: An effect. + public static func catching(_ work: @escaping () throws -> Value) -> Self { + .future { $0(Result { try work() }) } + } +} + +extension SignalProducer { + /// Turns any publisher into an ``Effect``. + /// + /// This can be useful for when you perform a chain of publisher transformations in a reducer, and + /// you need to convert that publisher to an effect so that you can return it from the reducer: + /// + /// ```swift + /// case .buttonTapped: + /// return fetchUser(id: 1) + /// .filter(\.isAdmin) + /// .eraseToEffect() + /// ``` + /// + /// - Returns: An effect that wraps `self`. + public func eraseToEffect() -> Self { + self } + /// Turns any publisher into an ``Effect`` that cannot fail by wrapping its output and failure in + /// a result. + /// + /// This can be useful when you are working with a failing API but want to deliver its data to an + /// action that handles both success and failure. + /// + /// ```swift + /// case .buttonTapped: + /// return environment.fetchUser(id: 1) + /// .catchToEffect() + /// .map(ProfileAction.userResponse) + /// ``` + /// + /// - Returns: An effect that wraps `self`. public func catchToEffect() -> Effect, Never> { self.map(Result.success) .flatMapError { Effect, Never>(value: Result.failure($0)) } } + /// Turns any publisher into an ``Effect`` that cannot fail by wrapping its output and failure + /// into a result and then applying passed in function to it. + /// + /// This is a convenience operator for writing ``Effect/catchToEffect()`` followed by a + /// ``Effect/map(_:)``. + /// + /// ```swift + /// case .buttonTapped: + /// return environment.fetchUser(id: 1) + /// .catchToEffect(ProfileAction.userResponse) + /// ``` + /// + /// - Parameters: + /// - transform: A mapping function that converts `Result` to another type. + /// - Returns: An effect that wraps `self`. public func catchToEffect( _ transform: @escaping (Result) -> T ) -> Effect { @@ -66,6 +309,22 @@ extension Effect { .flatMapError { Effect(value: transform(.failure($0))) } } + /// Turns any publisher into an ``Effect`` for any output and failure type by ignoring all output + /// and any failure. + /// + /// This is useful for times you want to fire off an effect but don't want to feed any data back + /// into the system. It can automatically promote an effect to your reducer's domain. + /// + /// ```swift + /// case .buttonTapped: + /// return analyticsClient.track("Button Tapped") + /// .fireAndForget() + /// ``` + /// + /// - Parameters: + /// - outputType: An output type. + /// - failureType: A failure type. + /// - Returns: An effect that never produces output or errors. public func fireAndForget( outputType: NewValue.Type = NewValue.self, failureType: NewError.Type = NewError.self @@ -75,13 +334,16 @@ extension Effect { .empty } } - - public func eraseToEffect() -> Self { - self - } } extension Effect where Self.Error == Never { + + /// Assigns each element from a observable to a property on an object. + /// + /// - Parameters: + /// - to: A key path that indicates the property to assign. See Key-Path Expression in The Swift Programming Language to learn how to use key paths to specify a property of an object. + /// - object: The object that contains the property. The subscriber assigns the object’s property every time it receives a new value. + /// - Returns: An Disposable instance. Call dispose() on this instance when you no longer want the publisher to automatically assign the property. Deinitializing this instance will also dispose automatic assignment. @discardableResult public func assign(to keyPath: ReferenceWritableKeyPath, on object: Root) -> Disposable diff --git a/Sources/ComposableArchitecture/Effects/Cancellation.swift b/Sources/ComposableArchitecture/Effects/Cancellation.swift index efd474c..732467c 100644 --- a/Sources/ComposableArchitecture/Effects/Cancellation.swift +++ b/Sources/ComposableArchitecture/Effects/Cancellation.swift @@ -1,17 +1,33 @@ 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 { + /// Turns an effect into one that is capable of being canceled. + /// + /// To turn an effect into a cancellable one you must provide an identifier, which is used in + /// ``Effect/cancel(id:)`` to identify which in-flight effect should be canceled. Any hashable + /// value can be used for the identifier, such as a string, but you can add a bit of protection + /// against typos by defining a new type that conforms to `Hashable`, such as an empty struct: + /// + /// ```swift + /// struct LoadUserId: Hashable {} + /// + /// case .reloadButtonTapped: + /// // Start a new effect to load the user + /// return environment.loadUser + /// .map(Action.userResponse) + /// .cancellable(id: LoadUserId(), cancelInFlight: true) + /// + /// case .cancelButtonTapped: + /// // Cancel any in-flight requests to load the user + /// return .cancel(id: LoadUserId()) + /// ``` + /// + /// - Parameters: + /// - id: The effect's identifier. + /// - cancelInFlight: Determines if any in-flight effect with the same identifier should be + /// canceled before starting this new one. + /// - Returns: A new effect that is capable of being canceled by an identifier. public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Effect { let effect = Effect.deferred { () -> SignalProducer in cancellablesLock.lock() @@ -60,6 +76,11 @@ extension Effect { return cancelInFlight ? .concatenate(.cancel(id: id), effect) : effect } + /// An effect that will cancel any currently in-flight effect with the given identifier. + /// + /// - Parameter id: An effect identifier. + /// - Returns: A new effect that will cancel any currently in-flight effect with the given + /// identifier. public static func cancel(id: AnyHashable) -> Effect { return .fireAndForget { cancellablesLock.sync { @@ -68,10 +89,20 @@ extension Effect { } } + /// An effect that will cancel multiple currently in-flight effects with the given identifiers. + /// + /// - Parameter ids: A variadic list of effect identifiers. + /// - Returns: A new effect that will cancel any currently in-flight effects with the given + /// identifiers. public static func cancel(ids: AnyHashable...) -> Effect { .cancel(ids: ids) } + /// An effect that will cancel multiple currently in-flight effects with the given identifiers. + /// + /// - Parameter ids: An array of effect identifiers. + /// - Returns: A new effect that will cancel any currently in-flight effects with the given + /// identifiers. public static func cancel(ids: [AnyHashable]) -> Effect { .merge(ids.map(Effect.cancel(id:))) } diff --git a/Sources/ComposableArchitecture/Effects/Concurrency.swift b/Sources/ComposableArchitecture/Effects/Concurrency.swift new file mode 100644 index 0000000..df313bb --- /dev/null +++ b/Sources/ComposableArchitecture/Effects/Concurrency.swift @@ -0,0 +1,109 @@ +import ReactiveSwift +import SwiftUI +import Combine + +#if compiler(>=5.5) && canImport(_Concurrency) +@available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) +extension Effect { + /// Wraps an asynchronous unit of work in an effect. + /// + /// This function is useful for executing work in an asynchronous context and capture the + /// result in an ``Effect`` so that the reducer, a non-asynchronous context, can process it. + /// + /// ```swift + /// Effect.task { + /// guard case let .some((data, _)) = try? await URLSession.shared + /// .data(from: .init(string: "http://numbersapi.com/42")!) + /// else { + /// return "Could not load" + /// } + /// + /// return String(decoding: data, as: UTF8.self) + /// } + /// ``` + /// + /// Note that due to the lack of tools to control the execution of asynchronous work in Swift, + /// it is not recommended to use this function in reducers directly. Doing so will introduce + /// thread hops into your effects that will make testing difficult. You will be responsible + /// for adding explicit expectations to wait for small amounts of time so that effects can + /// deliver their output. + /// + /// Instead, this function is most helpful for calling `async`/`await` functions from the live + /// implementation of dependencies, such as `URLSession.data`, `MKLocalSearch.start` and more. + /// + /// - Parameters: + /// - priority: Priority of the underlying task. If `nil`, the priority will come from + /// `Task.currentPriority`. + /// - operation: The operation to execute. + /// - Returns: An effect wrapping the given asynchronous work. + 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() }) + } + + /// Wraps an asynchronous unit of work in an effect. + /// + /// This function is useful for executing work in an asynchronous context and capture the + /// result in an ``Effect`` so that the reducer, a non-asynchronous context, can process it. + /// + /// ```swift + /// Effect.task { + /// let (data, _) = try await URLSession.shared + /// .data(from: .init(string: "http://numbersapi.com/42")!) + /// + /// return String(decoding: data, as: UTF8.self) + /// } + /// ``` + /// + /// Note that due to the lack of tools to control the execution of asynchronous work in Swift, + /// it is not recommended to use this function in reducers directly. Doing so will introduce + /// thread hops into your effects that will make testing difficult. You will be responsible + /// for adding explicit expectations to wait for small amounts of time so that effects can + /// deliver their output. + /// + /// Instead, this function is most helpful for calling `async`/`await` functions from the live + /// implementation of dependencies, such as `URLSession.data`, `MKLocalSearch.start` and more. + /// + /// - Parameters: + /// - priority: Priority of the underlying task. If `nil`, the priority will come from + /// `Task.currentPriority`. + /// - operation: The operation to execute. + /// - Returns: An effect wrapping the given asynchronous work. + 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) + } + } +} +#endif diff --git a/Sources/ComposableArchitecture/Effects/Debouncing.swift b/Sources/ComposableArchitecture/Effects/Debouncing.swift index e5d04fe..8081d72 100644 --- a/Sources/ComposableArchitecture/Effects/Debouncing.swift +++ b/Sources/ComposableArchitecture/Effects/Debouncing.swift @@ -2,6 +2,29 @@ import Foundation import ReactiveSwift extension Effect { + /// Turns an effect into one that can be debounced. + /// + /// To turn an effect into a debounce-able one you must provide an identifier, which is used to + /// determine which in-flight effect should be canceled in order to start a new effect. Any + /// hashable value can be used for the identifier, such as a string, but you can add a bit of + /// protection against typos by defining a new type that conforms to `Hashable`, such as an empty + /// struct: + /// + /// ```swift + /// case let .textChanged(text): + /// struct SearchId: Hashable {} + /// + /// return environment.search(text) + /// .debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue) + /// .map(Action.searchResponse) + /// ``` + /// + /// - Parameters: + /// - id: The effect's identifier. + /// - dueTime: The duration you want to debounce for. + /// - scheduler: The scheduler you want to deliver the debounced output to. + /// - options: Scheduler options that customize the effect's delivery of elements. + /// - Returns: An effect that publishes events only after a specified time elapses. public func debounce( id: AnyHashable, for dueTime: TimeInterval, diff --git a/Sources/ComposableArchitecture/Effects/Deferring.swift b/Sources/ComposableArchitecture/Effects/Deferring.swift index b5eca46..fac0288 100644 --- a/Sources/ComposableArchitecture/Effects/Deferring.swift +++ b/Sources/ComposableArchitecture/Effects/Deferring.swift @@ -2,6 +2,21 @@ import Foundation import ReactiveSwift extension Effect { + /// Returns an effect that will be executed after given `dueTime`. + /// + /// ```swift + /// case let .textChanged(text): + /// return environment.search(text) + /// .deferred(for: 0.5, scheduler: environment.mainQueue) + /// .map(Action.searchResponse) + /// ``` + /// + /// - Parameters: + /// - upstream: the effect you want to defer. + /// - dueTime: The duration you want to defer for. + /// - scheduler: The scheduler you want to deliver the defer output to. + /// - options: Scheduler options that customize the effect's delivery of elements. + /// - Returns: An effect that will be executed after `dueTime` public func deferred( for dueTime: TimeInterval, scheduler: DateScheduler diff --git a/Sources/ComposableArchitecture/Effects/Throttling.swift b/Sources/ComposableArchitecture/Effects/Throttling.swift index a54d2d2..420dba6 100644 --- a/Sources/ComposableArchitecture/Effects/Throttling.swift +++ b/Sources/ComposableArchitecture/Effects/Throttling.swift @@ -3,6 +3,17 @@ import Foundation import ReactiveSwift extension Effect { + /// Throttles an effect so that it only publishes one output per given interval. + /// + /// - Parameters: + /// - id: The effect's identifier. + /// - interval: The interval at which to find and emit the most recent element, expressed in + /// the time system of the scheduler. + /// - scheduler: The scheduler you want to deliver the throttled output to. + /// - latest: A boolean value that indicates whether to publish the most recent element. If + /// `false`, the publisher emits the first element received during the interval. + /// - Returns: An effect that emits either the most-recent or first element received during the + /// specified interval. public func throttle( id: AnyHashable, for interval: TimeInterval, diff --git a/Sources/ComposableArchitecture/Effects/Timer.swift b/Sources/ComposableArchitecture/Effects/Timer.swift index 5d11df9..09d4583 100644 --- a/Sources/ComposableArchitecture/Effects/Timer.swift +++ b/Sources/ComposableArchitecture/Effects/Timer.swift @@ -2,6 +2,99 @@ import Foundation import ReactiveSwift extension Effect where Value == Date, Error == Never { + /// Returns an effect that repeatedly emits the current time of the given scheduler on the given + /// interval. + /// + /// While it is possible to use Foundation's `Timer.publish(every:tolerance:on:in:options:)` API + /// to create a timer in the Composable Architecture, it is not advisable. This API only allows + /// creating a timer on a run loop, which means when writing tests you will need to explicitly + /// wait for time to pass in order to see how the effect evolves in your feature. + /// + /// In the Composable Architecture we test time-based effects like this by using the + /// `TestScheduler`, which allows us to explicitly and immediately advance time forward so that + /// we can see how effects emit. However, because `Timer.publish` takes a concrete `RunLoop` as + /// its scheduler, we can't substitute in a `TestScheduler` during tests`. + /// + /// That is why we provide the ``Effect/timer(id:every:tolerance:on:options:)`` effect. It allows you to create a timer that works + /// with any scheduler, not just a run loop, which means you can use a `DispatchQueue` or + /// `RunLoop` when running your live app, but use a `TestScheduler` in tests. + /// + /// To start and stop a timer in your feature you can create the timer effect from an action + /// and then use the ``Effect/cancel(id:)`` effect to stop the timer: + /// + /// ```swift + /// struct AppState { + /// var count = 0 + /// } + /// + /// enum AppAction { + /// case startButtonTapped, stopButtonTapped, timerTicked + /// } + /// + /// struct AppEnvironment { + /// var mainQueue: AnySchedulerOf + /// } + /// + /// let appReducer = Reducer { state, action, env in + /// struct TimerId: Hashable {} + /// + /// switch action { + /// case .startButtonTapped: + /// return Effect.timer(id: TimerId(), every: 1, on: env.mainQueue) + /// .map { _ in .timerTicked } + /// + /// case .stopButtonTapped: + /// return .cancel(id: TimerId()) + /// + /// case let .timerTicked: + /// state.count += 1 + /// return .none + /// } + /// ``` + /// + /// Then to test the timer in this feature you can use a test scheduler to advance time: + /// + /// ```swift + /// func testTimer() { + /// let scheduler = DispatchQueue.test + /// + /// let store = TestStore( + /// initialState: .init(), + /// reducer: appReducer, + /// environment: .init( + /// mainQueue: scheduler.eraseToAnyScheduler() + /// ) + /// ) + /// + /// store.send(.startButtonTapped) + /// + /// scheduler.advance(by: .seconds(1)) + /// store.receive(.timerTicked) { $0.count = 1 } + /// + /// scheduler.advance(by: .seconds(5)) + /// store.receive(.timerTicked) { $0.count = 2 } + /// store.receive(.timerTicked) { $0.count = 3 } + /// store.receive(.timerTicked) { $0.count = 4 } + /// store.receive(.timerTicked) { $0.count = 5 } + /// store.receive(.timerTicked) { $0.count = 6 } + /// + /// store.send(.stopButtonTapped) + /// } + /// ``` + /// + /// - Note: This effect is only meant to be used with features built in the Composable + /// Architecture, and returned from a reducer. If you want a testable alternative to + /// Foundation's `Timer.publish` you can use the publisher `Publishers.Timer` that is included + /// in this library via the + /// [`CombineSchedulers`](https://github.com/pointfreeco/combine-schedulers) module. + /// + /// - Parameters: + /// - interval: The time interval on which to publish events. For example, a value of `0.5` + /// publishes an event approximately every half-second. + /// - scheduler: The scheduler on which the timer runs. + /// - tolerance: The allowed timing variance when emitting events. Defaults to `nil`, which + /// allows any variance. + /// - options: Scheduler options passed to the timer. Defaults to `nil`. public static func timer( id: AnyHashable, every interval: DispatchTimeInterval, diff --git a/Sources/ComposableArchitecture/Internal/Binding+IsPresent.swift b/Sources/ComposableArchitecture/Internal/Binding+IsPresent.swift new file mode 100644 index 0000000..4d87c4c --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Binding+IsPresent.swift @@ -0,0 +1,13 @@ +import SwiftUI + +extension Binding { + func isPresent() -> Binding where Value == Wrapped? { + .init( + get: { self.wrappedValue != nil }, + set: { isPresent, transaction in + guard !isPresent else { return } + self.transaction(transaction).wrappedValue = nil + } + ) + } +} diff --git a/Sources/ComposableArchitecture/Internal/Breakpoint.swift b/Sources/ComposableArchitecture/Internal/Breakpoint.swift deleted file mode 100644 index 9fcaead..0000000 --- a/Sources/ComposableArchitecture/Internal/Breakpoint.swift +++ /dev/null @@ -1,32 +0,0 @@ -#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/Create.swift b/Sources/ComposableArchitecture/Internal/Create.swift new file mode 100644 index 0000000..4968221 --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/Create.swift @@ -0,0 +1,176 @@ + // https://github.com/CombineCommunity/CombineExt/blob/master/Sources/Operators/Create.swift + + // Copyright (c) 2020 Combine Community, and/or Shai Mishali + // + // Permission is hereby granted, free of charge, to any person obtaining a copy + // of this software and associated documentation files (the "Software"), to deal + // in the Software without restriction, including without limitation the rights + // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + // copies of the Software, and to permit persons to whom the Software is + // furnished to do so, subject to the following conditions: + // + // The above copyright notice and this permission notice shall be included in + // all copies or substantial portions of the Software. + // + // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + // THE SOFTWARE. + +import Combine +import Darwin + +final class DemandBuffer { + private var buffer = [S.Input]() + private let subscriber: S + private var completion: Subscribers.Completion? + private var demandState = Demand() + private let lock: os_unfair_lock_t + + init(subscriber: S) { + self.subscriber = subscriber + self.lock = os_unfair_lock_t.allocate(capacity: 1) + self.lock.initialize(to: os_unfair_lock()) + } + + deinit { + self.lock.deinitialize(count: 1) + self.lock.deallocate() + } + + func buffer(value: S.Input) -> Subscribers.Demand { + precondition( + self.completion == nil, "How could a completed publisher sent values?! Beats me 🤷‍♂️") + + switch demandState.requested { + case .unlimited: + return subscriber.receive(value) + default: + buffer.append(value) + return flush() + } + } + + func complete(completion: Subscribers.Completion) { + precondition( + self.completion == nil, "Completion have already occurred, which is quite awkward 🥺") + + self.completion = completion + _ = flush() + } + + func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand { + flush(adding: demand) + } + + private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand { + self.lock.sync { + + if let newDemand = newDemand { + demandState.requested += newDemand + } + + // If buffer isn't ready for flushing, return immediately + guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none } + + while !buffer.isEmpty && demandState.processed < demandState.requested { + demandState.requested += subscriber.receive(buffer.remove(at: 0)) + demandState.processed += 1 + } + + if let completion = completion { + // Completion event was already sent + buffer = [] + demandState = .init() + self.completion = nil + subscriber.receive(completion: completion) + return .none + } + + let sentDemand = demandState.requested - demandState.sent + demandState.sent += sentDemand + return sentDemand + } + } + + struct Demand { + var processed: Subscribers.Demand = .none + var requested: Subscribers.Demand = .none + var sent: Subscribers.Demand = .none + } +} + +extension Publishers { + fileprivate class Create: Publisher { + private let callback: (AnyPublisher.Subscriber) -> Cancellable + init(callback: @escaping (AnyPublisher.Subscriber) -> Cancellable) { + self.callback = callback + } + + func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { + subscriber.receive(subscription: Subscription(callback: callback, downstream: subscriber)) + } + } +} + +extension Publishers.Create { + fileprivate class Subscription: Combine.Subscription + where Output == Downstream.Input, Failure == Downstream.Failure { + private let buffer: DemandBuffer + private var cancellable: Cancellable? + + init( + callback: @escaping (AnyPublisher.Subscriber) -> Cancellable, + downstream: Downstream + ) { + self.buffer = DemandBuffer(subscriber: downstream) + let cancellable = callback( + .init( + send: { [weak self] in _ = self?.buffer.buffer(value: $0) }, + complete: { [weak self] in self?.buffer.complete(completion: $0) } + ) + ) + self.cancellable = cancellable + } + + func request(_ demand: Subscribers.Demand) { + _ = self.buffer.demand(demand) + } + + func cancel() { + self.cancellable?.cancel() + } + } +} + +extension Publishers.Create.Subscription: CustomStringConvertible { + var description: String { + return "Create.Subscription<\(Output.self), \(Failure.self)>" + } +} + +extension AnyPublisher { + public struct Subscriber { + private let _send: (Output) -> Void + private let _complete: (Subscribers.Completion) -> Void + + init( + send: @escaping (Output) -> Void, + complete: @escaping (Subscribers.Completion) -> Void + ) { + self._send = send + self._complete = complete + } + + public func send(_ value: Output) { + self._send(value) + } + + public func send(completion: Subscribers.Completion) { + self._complete(completion) + } + } +} diff --git a/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift b/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift new file mode 100644 index 0000000..90efc7e --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/CurrentValueRelay.swift @@ -0,0 +1,56 @@ +import Combine +import Foundation + +class CurrentValueRelay: Publisher { + typealias Failure = Never + + private var currentValue: Output + private var subscriptions: [Subscription>] = [] + + var value: Output { + get { self.currentValue } + set { self.send(newValue) } + } + + init(_ value: Output) { + self.currentValue = value + } + + func receive(subscriber: S) + where S: Subscriber, Never == S.Failure, Output == S.Input { + let subscription = Subscription(downstream: AnySubscriber(subscriber)) + self.subscriptions.append(subscription) + subscriber.receive(subscription: subscription) + subscription.forwardValueToBuffer(self.currentValue) + } + + func send(_ value: Output) { + self.currentValue = value + for subscription in subscriptions { + subscription.forwardValueToBuffer(value) + } + } +} + +extension CurrentValueRelay { + class Subscription: Combine.Subscription + where Output == Downstream.Input, Failure == Downstream.Failure { + private var demandBuffer: DemandBuffer? + + init(downstream: Downstream) { + self.demandBuffer = DemandBuffer(subscriber: downstream) + } + + func forwardValueToBuffer(_ value: Output) { + _ = demandBuffer?.buffer(value: value) + } + + func request(_ demand: Subscribers.Demand) { + _ = demandBuffer?.demand(demand) + } + + func cancel() { + demandBuffer = nil + } + } +} diff --git a/Sources/ComposableArchitecture/Internal/Debug.swift b/Sources/ComposableArchitecture/Internal/Debug.swift index ef7a3b2..7920e2a 100644 --- a/Sources/ComposableArchitecture/Internal/Debug.swift +++ b/Sources/ComposableArchitecture/Internal/Debug.swift @@ -1,159 +1,6 @@ +import CustomDump 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) @@ -161,56 +8,3 @@ extension String { } } -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 index f9b4b78..3ead27f 100644 --- a/Sources/ComposableArchitecture/Internal/Deprecations.swift +++ b/Sources/ComposableArchitecture/Internal/Deprecations.swift @@ -1,415 +1,647 @@ -#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) +//import CasePaths +//import Combine +//import SwiftUI +//import XCTestDynamicOverlay +// +//#if DEBUG +// import os +//#endif +// +//// NB: Deprecated after 0.31.0: +// +//extension Reducer { +// @available( +// *, +// deprecated, +// message: "'pullback' no longer takes a 'breakpointOnNil' argument" +// ) +// public func pullback( +// state toLocalState: CasePath, +// action toLocalAction: CasePath, +// environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, +// breakpointOnNil: Bool, +// file: StaticString = #fileID, +// line: UInt = #line +// ) -> Reducer { +// self.pullback( +// state: toLocalState, +// action: toLocalAction, +// environment: toLocalEnvironment, +// file: file, +// line: line +// ) +// } +// +// @available( +// *, +// deprecated, +// message: "'optional' no longer takes a 'breakpointOnNil' argument" +// ) +// public func optional( +// breakpointOnNil: Bool, +// file: StaticString = #fileID, +// line: UInt = #line +// ) -> Reducer< +// State?, Action, Environment +// > { +// self.optional(file: file, line: line) +// } +// +// @available( +// *, +// deprecated, +// message: "'forEach' no longer takes a 'breakpointOnNil' argument" +// ) +// public func forEach( +// state toLocalState: WritableKeyPath>, +// action toLocalAction: CasePath, +// environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, +// breakpointOnNil: Bool, +// file: StaticString = #fileID, +// line: UInt = #line +// ) -> Reducer { +// self.forEach( +// state: toLocalState, +// action: toLocalAction, +// environment: toLocalEnvironment, +// file: file, +// line: line +// ) +// } +// +// @available( +// *, +// deprecated, +// message: "'forEach' no longer takes a 'breakpointOnNil' argument" +// ) +// public func forEach( +// state toLocalState: WritableKeyPath, +// action toLocalAction: CasePath, +// environment toLocalEnvironment: @escaping (GlobalEnvironment) -> Environment, +// breakpointOnNil: Bool, +// file: StaticString = #fileID, +// line: UInt = #line +// ) -> Reducer { +// self.forEach( +// state: toLocalState, +// action: toLocalAction, +// environment: toLocalEnvironment, +// file: file, +// line: line +// ) // } +//} +// +//// NB: Deprecated after 0.29.0: +// +//#if DEBUG +// extension TestStore where LocalState: Equatable, Action: Equatable { +// @available( +// *, deprecated, message: "Use 'TestStore.send' and 'TestStore.receive' directly, instead" +// ) +// public func assert( +// _ steps: Step..., +// file: StaticString = #file, +// line: UInt = #line +// ) { +// assert(steps, file: file, line: line) +// } +// +// @available( +// *, deprecated, message: "Use 'TestStore.send' and 'TestStore.receive' directly, instead" +// ) +// public func assert( +// _ steps: [Step], +// file: StaticString = #file, +// line: UInt = #line +// ) { +// +// func assert(step: Step) { +// switch step.type { +// case let .send(action, update): +// self.send(action, file: step.file, line: step.line, update) +// +// case let .receive(expectedAction, update): +// self.receive(expectedAction, file: step.file, line: step.line, update) +// +// case let .environment(work): +// if !self.receivedActions.isEmpty { +// var actions = "" +// customDump(self.receivedActions.map(\.action), to: &actions) +// XCTFail( +// """ +// Must handle \(self.receivedActions.count) received \ +// action\(self.receivedActions.count == 1 ? "" : "s") before performing this work: … +// +// Unhandled actions: \(actions) +// """, +// file: step.file, line: step.line +// ) +// } +// do { +// try work(&self.environment) +// } catch { +// XCTFail("Threw error: \(error)", file: step.file, line: step.line) +// } +// +// case let .do(work): +// if !receivedActions.isEmpty { +// var actions = "" +// customDump(self.receivedActions.map(\.action), to: &actions) +// XCTFail( +// """ +// Must handle \(self.receivedActions.count) received \ +// action\(self.receivedActions.count == 1 ? "" : "s") before performing this work: … +// +// Unhandled actions: \(actions) +// """, +// file: step.file, line: step.line +// ) +// } +// do { +// try work() +// } catch { +// XCTFail("Threw error: \(error)", file: step.file, line: step.line) +// } +// +// case let .sequence(subSteps): +// subSteps.forEach(assert(step:)) +// } +// } // -// @_disfavoredOverload -// @available(*, deprecated, renamed: "producerScope(state:action:)") -// public func scope( -// state toLocalState: @escaping (Effect) -> Effect, +// steps.forEach(assert(step:)) +// +// self.completed() +// } +// +// public struct Step { +// fileprivate let type: StepType +// fileprivate let file: StaticString +// fileprivate let line: UInt +// +// private init( +// _ type: StepType, +// file: StaticString = #file, +// line: UInt = #line +// ) { +// self.type = type +// self.file = file +// self.line = line +// } +// +// @available(*, deprecated, message: "Call 'TestStore.send' directly, instead") +// public static func send( +// _ action: LocalAction, +// file: StaticString = #file, +// line: UInt = #line, +// _ update: @escaping (inout LocalState) throws -> Void = { _ in } +// ) -> Step { +// Step(.send(action, update), file: file, line: line) +// } +// +// @available(*, deprecated, message: "Call 'TestStore.receive' directly, instead") +// public static func receive( +// _ action: Action, +// file: StaticString = #file, +// line: UInt = #line, +// _ update: @escaping (inout LocalState) throws -> Void = { _ in } +// ) -> Step { +// Step(.receive(action, update), file: file, line: line) +// } +// +// @available(*, deprecated, message: "Mutate 'TestStore.environment' directly, instead") +// public static func environment( +// file: StaticString = #file, +// line: UInt = #line, +// _ update: @escaping (inout Environment) throws -> Void +// ) -> Step { +// Step(.environment(update), file: file, line: line) +// } +// +// @available(*, deprecated, message: "Perform this work directly in your test, instead") +// public static func `do`( +// file: StaticString = #file, +// line: UInt = #line, +// _ work: @escaping () throws -> Void +// ) -> Step { +// Step(.do(work), file: file, line: line) +// } +// +// @available(*, deprecated, message: "Perform this work directly in your test, instead") +// public static func sequence( +// _ steps: [Step], +// file: StaticString = #file, +// line: UInt = #line +// ) -> Step { +// Step(.sequence(steps), file: file, line: line) +// } +// +// @available(*, deprecated, message: "Perform this work directly in your test, instead") +// public static func sequence( +// _ steps: Step..., +// file: StaticString = #file, +// line: UInt = #line +// ) -> Step { +// Step(.sequence(steps), file: file, line: line) +// } +// +// fileprivate indirect enum StepType { +// case send(LocalAction, (inout LocalState) throws -> Void) +// case receive(Action, (inout LocalState) throws -> Void) +// case environment((inout Environment) throws -> Void) +// case `do`(() throws -> Void) +// case sequence([Step]) +// } +// } +// } +//#endif +// +//// NB: Deprecated after 0.27.1: +// +//extension AlertState.Button { +// @available( +// *, deprecated, message: "Cancel buttons must be given an explicit label as their first argument" +// ) +// public static func cancel(action: AlertState.ButtonAction? = nil) -> Self { +// .init(action: action, label: TextState("Cancel"), role: .cancel) +// } +//} +// +//@available(iOS 13, *) +//@available(macOS 12, *) +//@available(tvOS 13, *) +//@available(watchOS 6, *) +//@available(*, deprecated, renamed: "ConfirmationDialogState") +//public typealias ActionSheetState = ConfirmationDialogState +// +//extension View { +// @available(iOS 13, *) +// @available(macOS 12, *) +// @available(tvOS 13, *) +// @available(watchOS 6, *) +// @available(*, deprecated, renamed: "confirmationDialog") +// public func actionSheet( +// _ store: Store?, Action>, +// dismiss: Action +// ) -> some View { +// self.confirmationDialog(store, dismiss: dismiss) +// } +//} +// +//extension Store { +// @available( +// *, deprecated, +// message: +// "If you use this method, please open a discussion on GitHub and let us know how: https://github.com/pointfreeco/swift-composable-architecture/discussions/new" +// ) +// public func publisherScope( +// state toLocalState: @escaping (AnyPublisher) -> P, // action fromLocalAction: @escaping (LocalAction) -> Action -// ) -> Effect, Never> { -// self.producerScope(state: toLocalState, action: fromLocalAction) +// ) -> AnyPublisher, Never> +// where P.Output == LocalState, P.Failure == Never { +// +// func extractLocalState(_ state: State) -> LocalState? { +// var localState: LocalState? +// _ = toLocalState(Just(state).eraseToAnyPublisher()) +// .sink { localState = $0 } +// return localState +// } +// +// return toLocalState(self.state.eraseToAnyPublisher()) +// .map { localState in +// let localStore = Store( +// initialState: localState, +// reducer: .init { localState, localAction, _ in +// self.send(fromLocalAction(localAction)) +// localState = extractLocalState(self.state.value) ?? localState +// return .none +// }, +// environment: () +// ) +// +// localStore.parentCancellable = self.state +// .sink { [weak localStore] state in +// guard let localStore = localStore else { return } +// localStore.state.value = extractLocalState(state) ?? localStore.state.value +// } +// return localStore +// } +// .eraseToAnyPublisher() +// } +// +// @available( +// *, deprecated, +// message: +// "If you use this method, please open a discussion on GitHub and let us know how: https://github.com/pointfreeco/swift-composable-architecture/discussions/new" +// ) +// public func publisherScope( +// state toLocalState: @escaping (AnyPublisher) -> P +// ) -> AnyPublisher, Never> +// where P.Output == LocalState, P.Failure == Never { +// self.publisherScope(state: toLocalState, action: { $0 }) +// } +//} +// +//#if compiler(>=5.4) +// extension ViewStore { +// @available( +// *, deprecated, +// message: +// "Dynamic member lookup is no longer supported for bindable state. Instead of dot-chaining on the view store, e.g. 'viewStore.$value', invoke the 'binding' method on view store with a key path to the value, e.g. 'viewStore.binding(\\.$value)'. For more on this change, see: https://github.com/pointfreeco/swift-composable-architecture/pull/810" +// ) +// public subscript( +// dynamicMember keyPath: WritableKeyPath> +// ) -> Binding +// where Action: BindableAction, Action.State == State, Value: Equatable { +// self.binding( +// get: { $0[keyPath: keyPath].wrappedValue }, +// send: { .binding(.set(keyPath, $0)) } +// ) +// } +// } +//#endif +// +//// 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) +// } +// } +// } +// +// extension ViewStore { +// @available( +// *, deprecated, +// message: +// "For improved safety, bindable properties must now be wrapped explicitly in 'BindableState'. Bindings are now derived via 'ViewStore.binding' with a key path to that 'BindableState' (for example, 'viewStore.binding(\\.$value)'). For dynamic member lookup to be available, the view store's 'Action' type must also conform to '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)) } +// ) +// } +// } +//#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 'ViewStore.binding' with a key path to that 'BindableState' (for example, 'viewStore.binding(\\.$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: +// +//extension AlertState.Button { +// @available(*, deprecated, renamed: "cancel(_:action:)") +// public static func cancel( +// _ label: TextState, +// send action: Action? +// ) -> Self { +// .cancel(label, action: action.map(AlertState.ButtonAction.send)) +// } +// +// @available(*, deprecated, renamed: "cancel(action:)") +// public static func cancel( +// send action: Action? +// ) -> Self { +// .cancel(action: action.map(AlertState.ButtonAction.send)) +// } +// +// @available(*, deprecated, renamed: "default(_:action:)") +// public static func `default`( +// _ label: TextState, +// send action: Action? +// ) -> Self { +// .default(label, action: action.map(AlertState.ButtonAction.send)) +// } +// +// @available(*, deprecated, renamed: "destructive(_:action:)") +// public static func destructive( +// _ label: TextState, +// send action: Action? +// ) -> Self { +// .destructive(label, action: action.map(AlertState.ButtonAction.send)) // } //} // -//// NB: Deprecated after 0.6.0: +//// NB: Deprecated after 0.20.0: // //extension Reducer { -// @available(*, deprecated, renamed: "optional()") -// public var optional: Reducer { -// self.optional() +// @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 DEBUG +// os_log( +// .fault, dso: rw.dso, log: rw.log, +// """ +// A "forEach" reducer at "%@:%d" received an action when state contained no element at \ +// that index. … +// +// Action: +// %@ +// Index: +// %d +// +// 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". +// """, +// "\(file)", +// line, +// debugCaseOutput(localAction), +// index +// ) +// #endif +// return .none +// } +// return self.run( +// &globalState[keyPath: toLocalState][index], +// localAction, +// toLocalEnvironment(globalEnvironment) +// ) +// .map { toLocalAction.embed((index, $0)) } +// } +// } +//} +// +//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) // } //} +// diff --git a/Sources/ComposableArchitecture/Internal/Locking.swift b/Sources/ComposableArchitecture/Internal/Locking.swift index 76c316e..5108c88 100644 --- a/Sources/ComposableArchitecture/Internal/Locking.swift +++ b/Sources/ComposableArchitecture/Internal/Locking.swift @@ -1,15 +1,13 @@ 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() - } +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 diff --git a/Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift b/Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift new file mode 100644 index 0000000..ac9bb88 --- /dev/null +++ b/Sources/ComposableArchitecture/Internal/RuntimeWarnings.swift @@ -0,0 +1,26 @@ +#if DEBUG + import os + + // NB: Xcode runtime warnings offer a much better experience than traditional assertions and + // breakpoints, but Apple provides no means of creating custom runtime warnings ourselves. + // To work around this, we hook into SwiftUI's runtime issue delivery mechanism, instead. + // + // Feedback filed: https://gist.github.com/stephencelis/a8d06383ed6ccde3e5ef5d1b3ad52bbc + let rw = ( + dso: { () -> UnsafeMutableRawPointer in + let count = _dyld_image_count() + for i in 0.. { private let reducer: (inout State, Action, Environment) -> Effect - + + /// Initializes a reducer from a simple reducer function signature. + /// + /// The reducer takes three arguments: state, action and environment. The state is `inout` so that + /// you can make any changes to it directly inline. The reducer must return an effect, which + /// typically would be constructed by using the dependencies inside the `environment` value. If + /// no effect needs to be executed, a ``Effect/none`` effect can be returned. + /// + /// For example: + /// + /// ```swift + /// struct MyState { var count = 0, text = "" } + /// enum MyAction { case buttonTapped, textChanged(String) } + /// struct MyEnvironment { var analyticsClient: AnalyticsClient } + /// + /// let myReducer = Reducer { state, action, environment in + /// switch action { + /// case .buttonTapped: + /// state.count += 1 + /// return environment.analyticsClient.track("Button Tapped") + /// + /// case .textChanged(let text): + /// state.text = text + /// return .none + /// } + /// } + /// ``` + /// + /// - Parameter reducer: A function signature that takes state, action and + /// environment. public init(_ reducer: @escaping (inout State, Action, Environment) -> Effect) { self.reducer = reducer } - + + /// A reducer that performs no state mutations and returns no effects. public static var empty: Reducer { Self { _, _, _ in .none } } - + + /// Combines many reducers into a single one by running each one on state in order, and merging + /// all of the effects. + /// + /// It is important to note that the order of combining reducers matter. Combining `reducerA` with + /// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`. + /// + /// This can become an issue when working with reducers that have overlapping domains. For + /// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies + /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state + /// _before_ or _after_ `reducerB` runs. + /// + /// This is perhaps most easily seen when working with ``optional(file:line:)`` reducers, where + /// the parent domain may listen to the child domain and `nil` out its state. If the parent + /// reducer runs before the child reducer, then the child reducer will not be able to react to its + /// own action. + /// + /// Similar can be said for a ``forEach(state:action:environment:file:line:)-gvte`` reducer. If + /// the parent domain modifies the child collection by moving, removing, or modifying an element + /// before the ``forEach(state:action:environment:file:line:)-gvte`` reducer runs, the + /// ``forEach(state:action:environment:file:line:)-gvte`` reducer may perform its action against + /// the wrong element, an element that no longer exists, or an element in an unexpected state. + /// + /// Running a parent reducer before a child reducer can be considered an application logic + /// error, and can produce assertion failures. So you should almost always combine reducers in + /// order from child to parent domain. + /// + /// Here is an example of how you should combine an ``optional(file:line:)`` reducer with a parent + /// domain: + /// + /// ```swift + /// let parentReducer = Reducer.combine( + /// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`. + /// childReducer.optional().pullback( + /// state: \.child, + /// action: /ParentAction.child, + /// environment: { $0.child } + /// ), + /// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`. + /// Reducer { state, action, environment in + /// switch action + /// case .child(.dismiss): + /// state.child = nil + /// return .none + /// ... + /// } + /// }, + /// ) + /// ``` + /// + /// - Parameter reducers: A list of reducers. + /// - Returns: A single reducer. public static func combine(_ reducers: Reducer...) -> Reducer { .combine(reducers) } - + + /// Combines many reducers into a single one by running each one on state in order, and merging + /// all of the effects. + /// + /// It is important to note that the order of combining reducers matter. Combining `reducerA` with + /// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`. + /// + /// This can become an issue when working with reducers that have overlapping domains. For + /// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies + /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state + /// _before_ or _after_ `reducerB` runs. + /// + /// This is perhaps most easily seen when working with ``optional(file:line:)`` reducers, where + /// the parent domain may listen to the child domain and `nil` out its state. If the parent + /// reducer runs before the child reducer, then the child reducer will not be able to react to its + /// own action. + /// + /// Similar can be said for a ``forEach(state:action:environment:file:line:)-gvte`` reducer. If + /// the parent domain modifies the child collection by moving, removing, or modifying an element + /// before the ``forEach(state:action:environment:file:line:)-gvte`` reducer runs, the + /// ``forEach(state:action:environment:file:line:)-gvte`` reducer may perform its action against + /// the wrong element, an element that no longer exists, or an element in an unexpected state. + /// + /// Running a parent reducer before a child reducer can be considered an application logic error, + /// and can produce assertion failures. So you should almost always combine reducers in order from + /// child to parent domain. + /// + /// Here is an example of how you should combine an ``optional(file:line:)`` reducer with a parent + /// domain: + /// + /// ```swift + /// let parentReducer = Reducer.combine( + /// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`. + /// childReducer.optional().pullback( + /// state: \.child, + /// action: /ParentAction.child, + /// environment: { $0.child } + /// ), + /// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`. + /// Reducer { state, action, environment in + /// switch action + /// case .child(.dismiss): + /// state.child = nil + /// return .none + /// ... + /// } + /// }, + /// ) + /// ``` + /// + /// - Parameter reducers: An array of reducers. + /// - Returns: A single reducer. public static func combine(_ reducers: [Reducer]) -> Reducer { Self { value, action, environment in - .merge(reducers.map { $0.reducer(&value, action, environment) }) + .merge(reducers.map { $0.reducer(&value, action, environment) }) } } - + + /// Combines many reducers into a single one by running each one on state in order, and merging + /// all of the effects. + /// + /// It is important to note that the order of combining reducers matter. Combining `reducerA` with + /// `reducerB` is not necessarily the same as combining `reducerB` with `reducerA`. + /// + /// This can become an issue when working with reducers that have overlapping domains. For + /// example, if `reducerA` embeds the domain of `reducerB` and reacts to its actions or modifies + /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state + /// _before_ or _after_ `reducerB` runs. + /// + /// This is perhaps most easily seen when working with ``optional(file:line:)`` reducers, where + /// the parent domain may listen to the child domain and `nil` out its state. If the parent + /// reducer runs before the child reducer, then the child reducer will not be able to react to its + /// own action. + /// + /// Similar can be said for a ``forEach(state:action:environment:file:line:)-gvte`` reducer. If + /// the parent domain modifies the child collection by moving, removing, or modifying an element + /// before the ``forEach(state:action:environment:file:line:)-gvte`` reducer runs, the + /// ``forEach(state:action:environment:file:line:)-gvte`` reducer may perform its action against + /// the wrong element, an element that no longer exists, or an element in an unexpected state. + /// + /// Running a parent reducer before a child reducer can be considered an application logic error, + /// and can produce assertion failures. So you should almost always combine reducers in order from + /// child to parent domain. + /// + /// Here is an example of how you should combine an ``optional(file:line:)`` reducer with a parent + /// domain: + /// + /// ```swift + /// let parentReducer: Reducer = + /// // Run before parent so that it can react to `.dismiss` while state is non-`nil`. + /// childReducer + /// .optional() + /// .pullback( + /// state: \.child, + /// action: /ParentAction.child, + /// environment: { $0.child } + /// ) + /// // Combined after child so that it can `nil` out child state upon `.child(.dismiss)`. + /// .combined( + /// with: Reducer { state, action, environment in + /// switch action + /// case .child(.dismiss): + /// state.child = nil + /// return .none + /// ... + /// } + /// } + /// ) + /// ``` + /// + /// - Parameter other: Another reducer. + /// - Returns: A single reducer. public func combined(with other: Reducer) -> Reducer { .combine(self, other) } - + + /// Transforms a reducer that works on local state, action, and environment into one that works on + /// global state, action and environment. It accomplishes this by providing 3 transformations to + /// the method: + /// + /// * A writable key path that can get/set a piece of local state from the global state. + /// * A case path that can extract/embed a local action into a global action. + /// * A function that can transform the global environment into a local environment. + /// + /// This operation is important for breaking down large reducers into small ones. When used with + /// the ``combine(_:)-1ern2`` operator you can define many reducers that work on small pieces of + /// domain, and then _pull them back_ and _combine_ them into one big reducer that works on a + /// large domain. + /// + /// ```swift + /// // Global domain that holds a local domain: + /// struct AppState { var settings: SettingsState, /* rest of state */ } + /// enum AppAction { case settings(SettingsAction), /* other actions */ } + /// struct AppEnvironment { var settings: SettingsEnvironment, /* rest of dependencies */ } + /// + /// // A reducer that works on the local domain: + /// let settingsReducer = Reducer { ... } + /// + /// // Pullback the settings reducer so that it works on all of the app domain: + /// let appReducer: Reducer = .combine( + /// settingsReducer.pullback( + /// state: \.settings, + /// action: /AppAction.settings, + /// environment: { $0.settings } + /// ), + /// + /// /* other reducers */ + /// ) + /// ``` + /// + /// - Parameters: + /// - toLocalState: A key path that can get/set `State` inside `GlobalState`. + /// - toLocalAction: A case path that can extract/embed `Action` from `GlobalAction`. + /// - toLocalEnvironment: A function that transforms `GlobalEnvironment` into `Environment`. + /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`. public func pullback( state toLocalState: WritableKeyPath, action toLocalAction: CasePath, @@ -37,64 +283,393 @@ public struct Reducer { localAction, toLocalEnvironment(globalEnvironment) ) - .map(toLocalAction.embed) + .map(toLocalAction.embed) } } - + + /// Transforms a reducer that works on local state, action, and environment into one that works on + /// global state, action and environment. + /// + /// It accomplishes this by providing 3 transformations to the method: + /// + /// * A case path that can extract/embed a piece of local state from the global state, which is + /// typically an enum. + /// * A case path that can extract/embed a local action into a global action. + /// * A function that can transform the global environment into a local environment. + /// + /// This overload of ``pullback(state:action:environment:)`` differs from the other in that it + /// takes a `CasePath` transformation for the state instead of a `WritableKeyPath`. This makes it + /// perfect for working on enum state as opposed to struct state. In particular, you can use this + /// operator to pullback a reducer that operates on a single case of some state enum to work on + /// the entire state enum. + /// + /// When used with the ``combine(_:)-994ak`` operator you can define many reducers that work each + /// case of the state enum, and then _pull them back_ and _combine_ them into one big reducer that + /// works on a large domain. + /// + /// ```swift + /// // Global domain that holds a local domain: + /// enum AppState { case loggedIn(LoggedInState), /* rest of state */ } + /// enum AppAction { case loggedIn(LoggedInAction), /* other actions */ } + /// struct AppEnvironment { var loggedIn: LoggedInEnvironment, /* rest of dependencies */ } + /// + /// // A reducer that works on the local domain: + /// let loggedInReducer = Reducer { ... } + /// + /// // Pullback the logged-in reducer so that it works on all of the app domain: + /// let appReducer: Reducer = .combine( + /// loggedInReducer.pullback( + /// state: /AppState.loggedIn, + /// action: /AppAction.loggedIn, + /// environment: { $0.loggedIn } + /// ), + /// + /// /* other reducers */ + /// ) + /// ``` + /// + /// Take care when combining a child reducer for a particular case of enum state into its parent + /// domain. A child reducer cannot process actions in its domain if it fails to extract its + /// corresponding state. If a child action is sent to a reducer when its state is unavailable, it + /// is generally considered a logic error, and a runtime warning will be logged. There are a few + /// ways in which these errors can sneak into a code base: + /// + /// * A parent reducer sets child state to a different case when processing a child action and + /// runs _before_ the child reducer: + /// + /// ```swift + /// let parentReducer = Reducer.combine( + /// // When combining reducers, the parent reducer runs first + /// Reducer { state, action, environment in + /// switch action { + /// case .child(.didDisappear): + /// // And `nil`s out child state when processing a child action + /// state.child = .anotherChild(AnotherChildState()) + /// return .none + /// ... + /// } + /// }, + /// // Before the child reducer runs + /// childReducer.pullback(state: /ParentState.child, ...) + /// ) + /// + /// let childReducer = Reducer< + /// ChildState, ChildAction, ChildEnvironment + /// > { state, action environment in + /// case .didDisappear: + /// // This action is never received here because child state cannot be extracted + /// ... + /// } + /// ``` + /// + /// To ensure that a child reducer can process any action that a parent may use to change its + /// state, combine it _before_ the parent: + /// + /// ```swift + /// let parentReducer = Reducer.combine( + /// // The child runs first + /// childReducer.pullback(state: /ParentState.child, ...), + /// // The parent runs after + /// Reducer { state, action, environment in + /// ... + /// } + /// ) + /// ``` + /// + /// * A child effect feeds a child action back into the store when child state is unavailable: + /// + /// ```swift + /// let childReducer = Reducer< + /// ChildState, ChildAction, ChildEnvironment + /// > { state, action environment in + /// switch action { + /// case .onAppear: + /// // An effect may want to later feed a result back to the child domain in an action + /// return environment.apiClient + /// .request() + /// .map(ChildAction.response) + /// + /// case let .response(response): + /// // But the child cannot process this action if its state is unavailable + /// ... + /// } + /// } + /// ``` + /// + /// It is perfectly reasonable to ignore the result of an effect when child state is `nil`, + /// for example one-off effects that you don't want to cancel. However, many long-living + /// effects _should_ be explicitly canceled when tearing down a child domain: + /// + /// ```swift + /// let childReducer = Reducer< + /// ChildState, ChildAction, ChildEnvironment + /// > { state, action environment in + /// struct MotionId: Hashable {} + /// + /// switch action { + /// case .onAppear: + /// // Mark long-living effects that shouldn't outlive their domain cancellable + /// return environment.motionClient + /// .start() + /// .map(ChildAction.motion) + /// .cancellable(id: MotionId()) + /// + /// case .onDisappear: + /// // And explicitly cancel them when the domain is torn down + /// return .cancel(id: MotionId()) + /// ... + /// } + /// } + /// ``` + /// + /// * A view store sends a child action when child state is `nil`: + /// + /// ```swift + /// WithViewStore(self.parentStore) { parentViewStore in + /// // If child state is `nil`, it cannot process this action. + /// Button("Child Action") { parentViewStore.send(.child(.action)) } + /// ... + /// } + /// ``` + /// + /// Use ``Store/scope(state:action:)`` with ``SwitchStore`` to ensure that views can only send + /// child actions when the child domain is available. + /// + /// ```swift + /// SwitchStore(self.parentStore) { + /// CaseLet(state: /ParentState.child, action: ParentAction.child) { childStore in + /// // This destination only appears when child state matches + /// WithViewStore(childStore) { childViewStore in + /// // So this action can only be sent when child state is available + /// Button("Child Action") { childViewStore.send(.action) } + /// } + /// } + /// ... + /// } + /// ``` + /// + /// - See also: ``SwitchStore``, a SwiftUI helper for transforming a store on enum state into + /// stores on each case of the enum. + /// + /// - Parameters: + /// - toLocalState: A case path that can extract/embed `State` from `GlobalState`. + /// - toLocalAction: A case path that can extract/embed `Action` from `GlobalAction`. + /// - toLocalEnvironment: A function that transforms `GlobalEnvironment` into `Environment`. + /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`. 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( + #if DEBUG + os_log( + .fault, dso: rw.dso, log: rw.log, """ - --- - 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 \ + A reducer pulled back from "%@:%d" received an action when local state was \ + unavailable. … + + Action: + %@ + + 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 "%@" 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 \ + + • 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". - --- - """ + """, + "\(file)", + line, + debugCaseOutput(localAction), + "\(State.self)" ) - } + #endif return .none } defer { globalState = toLocalState.embed(localState) } + let effects = self.run( &localState, localAction, toLocalEnvironment(globalEnvironment) ) - .map(toLocalAction.embed) + .map(toLocalAction.embed) + return effects } } - + + /// Transforms a reducer that works on non-optional state into one that works on optional state by + /// only running the non-optional reducer when state is non-nil. + /// + /// Often used in tandem with ``pullback(state:action:environment:)`` to transform a reducer on a + /// non-optional child domain into a reducer that can be combined with a reducer on a parent + /// domain that contains some optional child domain: + /// + /// ```swift + /// // Global domain that holds an optional local domain: + /// struct AppState { var modal: ModalState? } + /// enum AppAction { case modal(ModalAction) } + /// struct AppEnvironment { var mainQueue: AnySchedulerOf } + /// + /// // A reducer that works on the non-optional local domain: + /// let modalReducer = Reducer.combine( + /// modalReducer.optional().pullback( + /// state: \.modal, + /// action: /AppAction.modal, + /// environment: { ModalEnvironment(mainQueue: $0.mainQueue) } + /// ), + /// Reducer { state, action, environment in + /// ... + /// } + /// ) + /// ``` + /// + /// Take care when combining optional reducers into parent domains. An optional reducer cannot + /// process actions in its domain when its state is `nil`. If a child action is sent to an + /// optional reducer when child state is `nil`, it is generally considered a logic error. There + /// are a few ways in which these errors can sneak into a code base: + /// + /// * A parent reducer sets child state to `nil` when processing a child action and runs + /// _before_ the child reducer: + /// + /// ```swift + /// let parentReducer = Reducer.combine( + /// // When combining reducers, the parent reducer runs first + /// Reducer { state, action, environment in + /// switch action { + /// case .child(.didDisappear): + /// // And `nil`s out child state when processing a child action + /// state.child = nil + /// return .none + /// ... + /// } + /// }, + /// // Before the child reducer runs + /// childReducer.optional().pullback(...) + /// ) + /// + /// let childReducer = Reducer< + /// ChildState, ChildAction, ChildEnvironment + /// > { state, action environment in + /// case .didDisappear: + /// // This action is never received here because child state is `nil` in the parent + /// ... + /// } + /// ``` + /// + /// To ensure that a child reducer can process any action that a parent may use to `nil` out + /// its state, combine it _before_ the parent: + /// + /// ```swift + /// let parentReducer = Reducer.combine( + /// // The child runs first + /// childReducer.optional().pullback(...), + /// // The parent runs after + /// Reducer { state, action, environment in + /// ... + /// } + /// ) + /// ``` + /// + /// * A child effect feeds a child action back into the store when child state is `nil`: + /// + /// ```swift + /// let childReducer = Reducer< + /// ChildState, ChildAction, ChildEnvironment + /// > { state, action environment in + /// switch action { + /// case .onAppear: + /// // An effect may want to feed its result back to the child domain in an action + /// return environment.apiClient + /// .request() + /// .map(ChildAction.response) + /// + /// case let .response(response): + /// // But the child cannot process this action if its state is `nil` in the parent + /// ... + /// } + /// } + /// ``` + /// + /// It is perfectly reasonable to ignore the result of an effect when child state is `nil`, + /// for example one-off effects that you don't want to cancel. However, many long-living + /// effects _should_ be explicitly canceled when tearing down a child domain: + /// + /// ```swift + /// let childReducer = Reducer< + /// ChildState, ChildAction, ChildEnvironment + /// > { state, action environment in + /// struct MotionId: Hashable {} + /// + /// switch action { + /// case .onAppear: + /// // Mark long-living effects that shouldn't outlive their domain cancellable + /// return environment.motionClient + /// .start() + /// .map(ChildAction.motion) + /// .cancellable(id: MotionId()) + /// + /// case .onDisappear: + /// // And explicitly cancel them when the domain is torn down + /// return .cancel(id: MotionId()) + /// ... + /// } + /// } + /// ``` + /// + /// * A view store sends a child action when child state is `nil`: + /// + /// ```swift + /// WithViewStore(self.parentStore) { parentViewStore in + /// // If child state is `nil`, it cannot process this action. + /// Button("Child Action") { parentViewStore.send(.child(.action)) } + /// ... + /// } + /// ``` + /// + /// Use ``Store/scope(state:action:)`` with ``IfLetStore`` or ``Store/ifLet(then:else:)`` to + /// ensure that views can only send child actions when the child domain is non-`nil`. + /// + /// ```swift + /// IfLetStore( + /// self.parentStore.scope(state: { $0.child }, action: { .child($0) } + /// ) { childStore in + /// // This destination only appears when child state is non-`nil` + /// WithViewStore(childStore) { childViewStore in + /// // So this action can only be sent when child state is non-`nil` + /// Button("Child Action") { childViewStore.send(.action) } + /// } + /// ... + /// } + /// ``` + /// + /// - See also: ``IfLetStore``, a SwiftUI helper for transforming a store on optional state into a + /// store on non-optional state. + /// - See also: ``Store/ifLet(then:else:)``, a UIKit helper for doing imperative work with a store + /// on optional state. + /// + /// - Returns: A reducer that works on optional state. public func optional( - breakpointOnNil: Bool = true, file: StaticString = #fileID, line: UInt = #line ) -> Reducer< @@ -102,81 +677,132 @@ public struct Reducer { > { .init { state, action, environment in guard state != nil else { - if breakpointOnNil { - breakpoint( + #if DEBUG + os_log( + .fault, dso: rw.dso, log: rw.log, """ - --- - 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 \ + An "optional" reducer at "%@:%d" received an action when state was "nil". … + + Action: + %@ + + 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 \ + "%@" 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 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". - --- - """ + """, + "\(file)", + line, + debugCaseOutput(action), + "\(State.self)" ) - } + #endif return .none } return self.reducer(&state!, action, environment) } } - + + /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on + /// an element into one that works on an identified array of elements. + /// + /// ```swift + /// // Global domain that holds a collection of local domains: + /// struct AppState { var todos: IdentifiedArrayOf } + /// enum AppAction { case todo(id: Todo.ID, action: TodoAction) } + /// struct AppEnvironment { var mainQueue: AnySchedulerOf } + /// + /// // A reducer that works on a local domain: + /// let todoReducer = Reducer { ... } + /// + /// // Pullback the local todo reducer so that it works on all of the app domain: + /// let appReducer = Reducer.combine( + /// todoReducer.forEach( + /// state: \.todos, + /// action: /AppAction.todo(id:action:), + /// environment: { _ in TodoEnvironment() } + /// ), + /// Reducer { state, action, environment in + /// ... + /// } + /// ) + /// ``` + /// + /// Take care when combining ``forEach(state:action:environment:file:line:)-gvte`` reducers into + /// parent domains, as order matters. Always combine + /// ``forEach(state:action:environment:file:line:)-gvte`` reducers _before_ parent reducers that + /// can modify the collection. + /// + /// - Parameters: + /// - toLocalState: A key path that can get/set a collection of `State` elements inside + /// `GlobalState`. + /// - toLocalAction: A case path that can extract/embed `(Collection.Index, Action)` from + /// `GlobalAction`. + /// - toLocalEnvironment: A function that transforms `GlobalEnvironment` into `Environment`. + /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`. 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( + #if DEBUG + os_log( + .fault, dso: rw.dso, log: rw.log, """ - --- - 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 \ + A "forEach" reducer at "%@:%d" received an action when state contained no element with \ + that id. … + + Action: + %@ + 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. \ + + • 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. \ + + • 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". - --- - """ + """, + "\(file)", + line, + debugCaseOutput(localAction), + "\(id)" ) - } + #endif return .none } - return self.reducer( + return + self + .reducer( &globalState[keyPath: toLocalState][id: id]!, localAction, toLocalEnvironment(globalEnvironment) @@ -184,46 +810,68 @@ public struct Reducer { .map { toLocalAction.embed((id, $0)) } } } - + + /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on + /// an element into one that works on a dictionary of element values. + /// + /// Take care when combining ``forEach(state:action:environment:file:line:)-21wow`` reducers into + /// parent domains, as order matters. Always combine + /// ``forEach(state:action:environment:file:line:)-21wow`` reducers _before_ parent reducers that + /// can modify the dictionary. + /// + /// - Parameters: + /// - toLocalState: A key path that can get/set a dictionary of `State` values inside + /// `GlobalState`. + /// - toLocalAction: A case path that can extract/embed `(Key, Action)` from `GlobalAction`. + /// - toLocalEnvironment: A function that transforms `GlobalEnvironment` into `Environment`. + /// - Returns: A reducer that works on `GlobalState`, `GlobalAction`, `GlobalEnvironment`. 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( + #if DEBUG + os_log( + .fault, dso: rw.dso, log: rw.log, """ - --- - 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 \ + A "forEach" reducer at "%@:%d" received an action when state contained no value at \ + that key. … + + Action: + %@ + 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 \ + + • 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 \ + + • 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. - --- - """ + """, + "\(file)", + line, + debugCaseOutput(localAction), + "\(key)" ) - } + #endif return .none } return self.reducer( @@ -231,10 +879,17 @@ public struct Reducer { localAction, toLocalEnvironment(globalEnvironment) ) - .map { toLocalAction.embed((key, $0)) } + .map { toLocalAction.embed((key, $0)) } } } - + + /// Runs the reducer. + /// + /// - Parameters: + /// - state: Mutable state. + /// - action: An action. + /// - environment: An environment. + /// - Returns: An effect that can emit zero or more actions. public func run( _ state: inout State, _ action: Action, @@ -242,7 +897,7 @@ public struct Reducer { ) -> Effect { self.reducer(&state, action, environment) } - + public func callAsFunction( _ state: inout State, _ action: Action, diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index ceeafcd..df9b18e 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -1,17 +1,155 @@ import Foundation import ReactiveSwift +#if DEBUG + import os +#endif + +/// A store represents the runtime that powers the application. It is the object that you will pass +/// around to views that need to interact with the application. +/// +/// You will typically construct a single one of these at the root of your application, and then use +/// the ``scope(state:action:)`` method to derive more focused stores that can be passed to +/// subviews: +/// +/// ```swift +/// @main +/// struct MyApp: App { +/// var body: some Scene { +/// WindowGroup { +/// RootView( +/// store: Store( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: AppEnvironment( +/// ... +/// ) +/// ) +/// ) +/// } +/// } +/// } +/// ``` +/// +/// ### Scoping +/// +/// The most important operation defined on ``Store`` is the ``scope(state:action:)`` method, which +/// allows you to transform a store into one that deals with local state and actions. This is +/// necessary for passing stores to subviews that only care about a small portion of the entire +/// application's domain. +/// +/// For example, if an application has a tab view at its root with tabs for activity, search, and +/// profile, then we can model the domain like this: +/// +/// ```swift +/// struct AppState { +/// var activity: ActivityState +/// var profile: ProfileState +/// var search: SearchState +/// } +/// +/// enum AppAction { +/// case activity(ActivityAction) +/// case profile(ProfileAction) +/// case search(SearchAction) +/// } +/// ``` +/// +/// We can construct a view for each of these domains by applying ``scope(state:action:)`` to a +/// store that holds onto the full app domain in order to transform it into a store for each +/// sub-domain: +/// +/// ```swift +/// struct AppView: View { +/// let store: Store +/// +/// var body: some View { +/// TabView { +/// ActivityView(store: self.store.scope(state: \.activity, action: AppAction.activity)) +/// .tabItem { Text("Activity") } +/// +/// SearchView(store: self.store.scope(state: \.search, action: AppAction.search)) +/// .tabItem { Text("Search") } +/// +/// ProfileView(store: self.store.scope(state: \.profile, action: AppAction.profile)) +/// .tabItem { Text("Profile") } +/// } +/// } +/// ``` +/// +/// ### Thread safety +/// +/// The `Store` class is not thread-safe, and so all interactions with an instance of ``Store`` +/// (including all of its scopes and derived ``ViewStore``s) must be done on the same thread the +/// store was created on. Further, if the store is powering a SwiftUI or UIKit view, as is +/// customary, then all interactions must be done on the _main_ thread. +/// +/// The reason stores are not thread-safe is due to the fact that when an action is sent to a store, +/// a reducer is run on the current state, and this process cannot be done from multiple threads. +/// It is possible to make this process thread-safe by introducing locks or queues, but this +/// introduces new complications: +/// +/// * If done simply with `DispatchQueue.main.async` you will incur a thread hop even when you are +/// already on the main thread. This can lead to unexpected behavior in UIKit and SwiftUI, where +/// sometimes you are required to do work synchronously, such as in animation blocks. +/// +/// * It is possible to create a scheduler that performs its work immediately when on the main +/// thread and otherwise uses `DispatchQueue.main.async` (e.g. see CombineScheduler's +/// [UIScheduler](https://github.com/pointfreeco/combine-schedulers/blob/main/Sources/CombineSchedulers/UIScheduler.swift)). +/// This introduces a lot more complexity, and should probably not be adopted without having a very +/// good reason. +/// +/// This is why we require all actions be sent from the same thread. This requirement is in the same +/// spirit of how `URLSession` and other Apple APIs are designed. Those APIs tend to deliver their +/// outputs on whatever thread is most convenient for them, and then it is your responsibility to +/// dispatch back to the main queue if that's what you need. The Composable Architecture makes you +/// responsible for making sure to send actions on the main thread. If you are using an effect that +/// may deliver its output on a non-main thread, you must explicitly perform `.receive(on:)` in +/// order to force it back on the main thread. +/// +/// This approach makes the fewest number of assumptions about how effects are created and +/// transformed, and prevents unnecessary thread hops and re-dispatching. It also provides some +/// testing benefits. If your effects are not responsible for their own scheduling, then in tests +/// all of the effects would run synchronously and immediately. You would not be able to test how +/// multiple in-flight effects interleave with each other and affect the state of your application. +/// However, by leaving scheduling out of the ``Store`` we get to test these aspects of our effects +/// if we so desire, or we can ignore if we prefer. We have that flexibility. +/// +/// #### Thread safety checks +/// +/// The store performs some basic thread safety checks in order to help catch mistakes. Stores +/// constructed via the initializer ``Store/init(initialState:reducer:environment:)`` are assumed +/// to run only on the main thread, and so a check is executed immediately to make sure that is the +/// case. Further, all actions sent to the store and all scopes (see ``Store/scope(state:action:)``) +/// of the store are also checked to make sure that work is performed on the main thread. +/// +/// If you need a store that runs on a non-main thread, which should be very rare and you should +/// have a very good reason to do so, then you can construct a store via the +/// ``Store/unchecked(initialState:reducer:environment:)`` static method to opt out of all main +/// thread checks. +/// +/// --- +/// +/// See also: ``ViewStore`` to understand how one observes changes to the state in a ``Store`` and +/// sends user actions. public final class Store { private var bufferedActions: [Action] = [] - var effectDisposables: [UUID: Disposable] = [:] + var effectCancellables: [UUID: Disposable] = [:] + var parentCancellable: Disposable? private var isSending = false - var parentDisposable: Disposable? private let reducer: (inout State, Action) -> Effect - var state: MutableProperty + @MutableProperty var state: State + #if DEBUG private let mainThreadChecksEnabled: Bool #endif + /// Initializes a store from an initial state, a reducer, and an environment. + /// + /// - Parameters: + /// - initialState: The state to start the application in. + /// - reducer: The reducer that powers the business logic of the application. + /// - environment: The environment of dependencies for the application. public convenience init( initialState: State, reducer: Reducer, @@ -26,6 +164,13 @@ public final class Store { self.threadCheck(status: .`init`) } + /// Initializes a store from an initial state, a reducer, and an environment, and the main thread + /// check is disabled for all interactions with this store. + /// + /// - Parameters: + /// - initialState: The state to start the application in. + /// - reducer: The reducer that powers the business logic of the application. + /// - environment: The environment of dependencies for the application. public static func unchecked( initialState: State, reducer: Reducer, @@ -39,33 +184,177 @@ public final class Store { ) } + /// Scopes the store to one that exposes local state and actions. + /// + /// This can be useful for deriving new stores to hand to child views in an application. For + /// example: + /// + /// ```swift + /// // Application state made from local states. + /// struct AppState { var login: LoginState, ... } + /// struct AppAction { case login(LoginAction), ... } + /// + /// // A store that runs the entire application. + /// let store = Store( + /// initialState: AppState(), + /// reducer: appReducer, + /// environment: AppEnvironment() + /// ) + /// + /// // Construct a login view by scoping the store to one that works with only login domain. + /// LoginView( + /// store: store.scope( + /// state: \.login, + /// action: AppAction.login + /// ) + /// ) + /// ``` + /// + /// Scoping in this fashion allows you to better modularize your application. In this case, + /// `LoginView` could be extracted to a module that has no access to `AppState` or `AppAction`. + /// + /// Scoping also gives a view the opportunity to focus on just the state and actions it cares + /// about, even if its feature domain is larger. + /// + /// For example, the above login domain could model a two screen login flow: a login form followed + /// by a two-factor authentication screen. The second screen's domain might be nested in the + /// first: + /// + /// ```swift + /// struct LoginState: Equatable { + /// var email = "" + /// var password = "" + /// var twoFactorAuth: TwoFactorAuthState? + /// } + /// + /// enum LoginAction: Equatable { + /// case emailChanged(String) + /// case loginButtonTapped + /// case loginResponse(Result) + /// case passwordChanged(String) + /// case twoFactorAuth(TwoFactorAuthAction) + /// } + /// ``` + /// + /// The login view holds onto a store of this domain: + /// ```swift + /// struct LoginView: View { + /// let store: Store + /// + /// var body: some View { ... } + /// } + /// ``` + /// + /// If its body were to use a view store of the same domain, this would introduce a number of + /// problems: + /// + /// * The login view would be able to read from `twoFactorAuth` state. This state is only intended + /// to be read from the two-factor auth screen. + /// + /// * Even worse, changes to `twoFactorAuth` state would now cause SwiftUI to recompute + /// `LoginView`'s body unnecessarily. + /// + /// * The login view would be able to send `twoFactorAuth` actions. These actions are only + /// intended to be sent from the two-factor auth screen (and reducer). + /// + /// * The login view would be able to send non user-facing login actions, like `loginResponse`. + /// These actions are only intended to be used in the login reducer to feed the results of + /// effects back into the store. + /// + /// To avoid these issues, one can introduce a view-specific domain that slices off the subset of + /// state and actions that a view cares about: + /// + /// ```swift + /// extension LoginView { + /// struct State: Equatable { + /// var email: String + /// var password: String + /// } + /// + /// enum Action: Equatable { + /// case emailChanged(String) + /// case loginButtonTapped + /// case passwordChanged(String) + /// } + /// } + /// ``` + /// + /// One can also introduce a couple helpers that transform feature state into view state and + /// transform view actions into feature actions. + /// + /// ```swift + /// extension LoginState { + /// var view: LoginView.State { + /// .init(email: self.email, password: self.password) + /// } + /// } + /// + /// extension LoginView.Action { + /// var feature: LoginAction { + /// switch self { + /// case let .emailChanged(email) + /// return .emailChanged(email) + /// case .loginButtonTapped: + /// return .loginButtonTapped + /// case let .passwordChanged(password) + /// return .passwordChanged(password) + /// } + /// } + /// } + /// ``` + /// + /// With these helpers defined, `LoginView` can now scope its store's feature domain into its view + /// domain: + /// + /// ```swift + /// var body: some View { + /// WithViewStore( + /// self.store.scope(state: \.view, action: \.feature) + /// ) { viewStore in + /// ... + /// } + /// } + /// ``` + /// + /// This view store is now incapable of reading any state but view state (and will not recompute + /// when non-view state changes), and is incapable of sending any actions but view actions. + /// + /// - Parameters: + /// - toLocalState: A function that transforms `State` into `LocalState`. + /// - fromLocalAction: A function that transforms `LocalAction` into `Action`. + /// - Returns: A new store with its domain (state and action) transformed. public func scope( state toLocalState: @escaping (State) -> LocalState, action fromLocalAction: @escaping (LocalAction) -> Action ) -> Store { + self.threadCheck(status: .scope) var isSending = false let localStore = Store( - initialState: toLocalState(self.state.value), + initialState: toLocalState(self.state), reducer: .init { localState, localAction, _ in isSending = true defer { isSending = false } self.send(fromLocalAction(localAction)) - localState = toLocalState(self.state.value) + localState = toLocalState(self.state) return .none }, environment: () ) - localStore.parentDisposable = self.state.producer + localStore.parentCancellable = self.$state.producer .skip(first: 1) .startWithValues { [weak localStore] newValue in guard !isSending else { return } - localStore?.state.value = toLocalState(newValue) + localStore?.state = toLocalState(newValue) } return localStore } + /// Scopes the store to one that exposes local state. + /// + /// - Parameter toLocalState: A function that transforms `State` into `LocalState`. + /// - Returns: A new store with its domain (state and action) transformed. public func scope( state toLocalState: @escaping (State) -> LocalState ) -> Store { @@ -74,14 +363,17 @@ public final class Store { 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 + var currentState = self.state defer { self.isSending = false - self.state.value = currentState + self.state = currentState } + while !self.bufferedActions.isEmpty { let action = self.bufferedActions.removeFirst() let effect = self.reducer(¤tState, action) @@ -94,35 +386,31 @@ public final class Store { completed: { [weak self] in self?.threadCheck(status: .effectCompletion(action)) didComplete = true - self?.effectDisposables.removeValue(forKey: uuid)?.dispose() + self?.effectCancellables.removeValue(forKey: uuid)?.dispose() }, interrupted: { [weak self] in didComplete = true - self?.effectDisposables.removeValue(forKey: uuid)?.dispose() + self?.effectCancellables.removeValue(forKey: uuid)?.dispose() } ) let effectDisposable = effect.start(observer) if !didComplete { - self.effectDisposables[uuid] = effectDisposable + self.effectCancellables[uuid] = effectDisposable } } } + /// Returns a "stateless" store by erasing state to `Void`. public var stateless: Store { self.scope(state: { _ in () }) } + /// Returns an "actionless" store by erasing action to `Never`. 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` @@ -136,62 +424,97 @@ public final class Store { 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. + os_log( + .fault, dso: rw.dso, log: rw.log, """ + An effect completed on a non-main thread. … + + Effect returned from: + %@ + + Make sure to use ".receive(on:)" on any effects that execute on background threads to \ + receive their output on the main thread, or create your store via "Store.unchecked" to \ + opt out of the main thread checker. + + 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 same \ + thread. + """, + debugCaseOutput(action) + ) 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. + os_log( + .fault, dso: rw.dso, log: rw.log, """ + A store initialized on a non-main thread. … + + If a store is intended to be used on a background thread, create it via \ + "Store.unchecked" to opt out of the main thread checker. + + 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 same \ + thread. + """ + ) 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. + os_log( + .fault, dso: rw.dso, log: rw.log, + """ + "Store.scope" was called on a non-main thread. … + + Make sure to use "Store.scope" on the main thread, or create your store via \ + "Store.unchecked" to opt out of the main thread checker. + + 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 same \ + thread. """ + ) 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. + os_log( + .fault, dso: rw.dso, log: rw.log, """ + "ViewStore.send" was called on a non-main thread with: %@ … + + Make sure that "ViewStore.send" is always called on the main thread, or create your \ + store via "Store.unchecked" to opt out of the main thread checker. + + 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 same \ + thread. + """, + debugCaseOutput(action) + ) 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. + os_log( + .fault, dso: rw.dso, log: rw.log, """ - } + An effect published an action on a non-main thread. … - breakpoint( - """ - --- - Warning: + Effect published: + %@ - A store created on the main thread was interacted with on a non-main thread: + Effect returned from: + %@ - Thread: \(Thread.current) + 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. - \(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. - --- - """ - ) + 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 same \ + thread. + """, + debugCaseOutput(action), + debugCaseOutput(originatingAction) + ) + } #endif } @@ -201,10 +524,17 @@ public final class Store { environment: Environment, mainThreadChecksEnabled: Bool ) { - self.state = MutableProperty(initialState) + self.state = initialState self.reducer = { state, action in reducer.run(&state, action, environment) } #if DEBUG self.mainThreadChecksEnabled = mainThreadChecksEnabled #endif } + + deinit { + for effect in effectCancellables.values { + effect.dispose() + } + self.parentCancellable?.dispose() + } } diff --git a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift b/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift deleted file mode 100644 index 23660b3..0000000 --- a/Sources/ComposableArchitecture/SwiftUI/ActionSheet.swift +++ /dev/null @@ -1,96 +0,0 @@ -#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 deleted file mode 100644 index d68236a..0000000 --- a/Sources/ComposableArchitecture/SwiftUI/ActionWrappingScheduler.swift +++ /dev/null @@ -1,107 +0,0 @@ -#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 index 7424943..cbe79d9 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Alert.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Alert.swift @@ -1,225 +1,431 @@ -#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 +import CustomDump +import SwiftUI + +/// A data type that describes the state of an alert that can be shown to the user. The `Action` +/// generic is the type of actions that can be sent from tapping on a button in the alert. +/// +/// This type can be used in your application's state in order to control the presentation or +/// dismissal of alerts. It is preferable to use this API instead of the default SwiftUI API +/// for alerts because SwiftUI uses 2-way bindings in order to control the showing and dismissal +/// of alerts, and that does not play nicely with the Composable Architecture. The library requires +/// that all state mutations happen by sending an action so that a reducer can handle that logic, +/// which greatly simplifies how data flows through your application, and gives you instant +/// testability on all parts of your application. +/// +/// To use this API, you model all the alert actions in your domain's action enum: +/// +/// ```swift +/// enum AppAction: Equatable { +/// case cancelTapped +/// case confirmTapped +/// case deleteTapped +/// +/// // Your other actions +/// } +/// ``` +/// +/// And you model the state for showing the alert in your domain's state, and it can start off +/// `nil`: +/// +/// ```swift +/// struct AppState: Equatable { +/// var alert: AlertState? +/// +/// // Your other state +/// } +/// ``` +/// +/// Then, in the reducer you can construct an ``AlertState`` value to represent the alert you want +/// to show to the user: +/// +/// ```swift +/// let appReducer = Reducer { state, action, env in +/// switch action +/// case .cancelTapped: +/// state.alert = nil +/// return .none +/// +/// case .confirmTapped: +/// state.alert = nil +/// // Do deletion logic... +/// +/// case .deleteTapped: +/// state.alert = .init( +/// title: TextState("Delete"), +/// message: TextState("Are you sure you want to delete this? It cannot be undone."), +/// primaryButton: .default(TextState("Confirm"), action: .send(.confirmTapped)), +/// secondaryButton: .cancel(TextState("Cancel")) +/// ) +/// return .none +/// } +/// } +/// ``` +/// +/// And then, in your view you can use the `.alert(_:send:dismiss:)` method on `View` in order +/// to present the alert in a way that works best with the Composable Architecture: +/// +/// ```swift +/// Button("Delete") { viewStore.send(.deleteTapped) } +/// .alert( +/// self.store.scope(state: \.alert), +/// dismiss: .cancelTapped +/// ) +/// ``` +/// +/// This makes your reducer in complete control of when the alert is shown or dismissed, and makes +/// it so that any choice made in the alert is automatically fed back into the reducer so that you +/// can handle its logic. +/// +/// Even better, you can instantly write tests that your alert behavior works as expected: +/// +/// ```swift +/// let store = TestStore( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: .mock +/// ) +/// +/// store.send(.deleteTapped) { +/// $0.alert = .init( +/// title: TextState("Delete"), +/// message: TextState("Are you sure you want to delete this? It cannot be undone."), +/// primaryButton: .default(TextState("Confirm"), action: .send(.confirmTapped)), +/// secondaryButton: .cancel(TextState("Cancel")) +/// ) +/// } +/// store.send(.deleteTapped) { +/// $0.alert = nil +/// // Also verify that delete logic executed correctly +/// } +/// ``` +/// +public struct AlertState { + public let id = UUID() + public var buttons: [Button] + public var message: TextState? + public var title: TextState + + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + public init( + title: TextState, + message: TextState? = nil, + buttons: [Button] + ) { + self.title = title + self.message = message + self.buttons = buttons + } + + public init( + title: TextState, + message: TextState? = nil, + dismissButton: Button? = nil + ) { + self.title = title + self.message = message + self.buttons = dismissButton.map { [$0] } ?? [] + } + + public init( + title: TextState, + message: TextState? = nil, + primaryButton: Button, + secondaryButton: Button + ) { + self.title = title + self.message = message + self.buttons = [primaryButton, secondaryButton] + } + + public struct Button { + public var action: ButtonAction? + public var label: TextState + public var role: ButtonRole? + + public static func cancel( + _ label: TextState, + action: ButtonAction? = nil + ) -> Self { + Self(action: action, label: label, role: .cancel) } - public init( - title: TextState, - message: TextState? = nil, - primaryButton: Button, - secondaryButton: Button - ) { - self.title = title - self.message = message - self.primaryButton = primaryButton - self.secondaryButton = secondaryButton + public static func `default`( + _ label: TextState, + action: ButtonAction? = nil + ) -> Self { + Self(action: action, label: label, role: nil) } - public struct Button { - public var action: ButtonAction? - public var type: ButtonType + public static func destructive( + _ label: TextState, + action: ButtonAction? = nil + ) -> Self { + Self(action: action, label: label, role: .destructive) + } + } - public static func cancel( - _ label: TextState, - action: ButtonAction? = nil - ) -> Self { - Self(action: action, type: .cancel(label: label)) - } + public struct ButtonAction { + public let type: ActionType - public static func cancel( - action: ButtonAction? = nil - ) -> Self { - Self(action: action, type: .cancel(label: nil)) - } + public static func send(_ action: Action) -> Self { + .init(type: .send(action)) + } - public static func `default`( - _ label: TextState, - action: ButtonAction? = nil - ) -> Self { - Self(action: action, type: .default(label: label)) - } + public static func send(_ action: Action, animation: Animation?) -> Self { + .init(type: .animatedSend(action, animation: animation)) + } - public static func destructive( - _ label: TextState, - action: ButtonAction? = nil - ) -> Self { - Self(action: action, type: .destructive(label: label)) - } + public enum ActionType { + case send(Action) + case animatedSend(Action, animation: Animation?) } + } - public struct ButtonAction { - let type: ActionType + public enum ButtonRole { + case cancel + case destructive - public static func send(_ action: Action) -> Self { - .init(type: .send(action)) + #if compiler(>=5.5) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + var toSwiftUI: SwiftUI.ButtonRole { + switch self { + case .cancel: + return .cancel + case .destructive: + return .destructive + } } + #endif + } +} - public static func send(_ action: Action, animation: Animation?) -> Self { - .init(type: .animatedSend(action, animation: animation)) +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. + @ViewBuilder public func alert( + _ store: Store?, Action>, + dismiss: Action + ) -> some View { + #if compiler(>=5.5) + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + self.modifier( + NewAlertModifier( + viewStore: ViewStore(store, removeDuplicates: { $0?.id == $1?.id }), + dismiss: dismiss + ) + ) + } else { + self.modifier( + OldAlertModifier( + viewStore: ViewStore(store, removeDuplicates: { $0?.id == $1?.id }), + dismiss: dismiss + ) + ) } + #else + self.modifier( + OldAlertModifier( + viewStore: ViewStore(store, removeDuplicates: { $0?.id == $1?.id }), + dismiss: dismiss + ) + ) + #endif + } +} - enum ActionType { - case send(Action) - case animatedSend(Action, animation: Animation?) - } - } +#if compiler(>=5.5) + // NB: Workaround for iOS 14 runtime crashes during iOS 15 availability checks. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + private struct NewAlertModifier: ViewModifier { + @ObservedObject var viewStore: ViewStore?, Action> + let dismiss: Action - public enum ButtonType { - case cancel(label: TextState?) - case `default`(label: TextState) - case destructive(label: TextState) + func body(content: Content) -> some View { + content.alert( + (viewStore.state?.title).map { Text($0) } ?? Text(""), + isPresented: viewStore.binding(send: dismiss).isPresent(), + presenting: viewStore.state, + actions: { $0.toSwiftUIActions(send: viewStore.send) }, + message: { $0.message.map { Text($0) } } + ) } } +#endif - @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) - } - } +private struct OldAlertModifier: ViewModifier { + @ObservedObject var viewStore: ViewStore?, Action> + let dismiss: Action + + func body(content: Content) -> some View { + content.alert(item: viewStore.binding(send: dismiss)) { state in + state.toSwiftUIAlert(send: viewStore.send) } } +} + +extension AlertState: CustomDumpReflectable { + public var customDumpMirror: Mirror { + Mirror( + self, + children: [ + "title": self.title, + "message": self.message as Any, + "buttons": self.buttons, + ], + displayStyle: .struct + ) + } +} + +extension AlertState.Button: CustomDumpReflectable { + public var customDumpMirror: Mirror { + Mirror( + self, + children: [ + self.role.map { "\($0)" } ?? "default": ( + self.label, + action: self.action + ) + ], + displayStyle: .enum + ) + } +} - @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 +extension AlertState.ButtonAction: CustomDumpReflectable { + public var customDumpMirror: Mirror { + switch self.type { + case let .send(action): + return Mirror( + self, + children: [ + "send": action + ], + displayStyle: .enum + ) + case let .animatedSend(action, animation): + return Mirror( + self, + children: [ + "send": (action, animation: animation) + ], + displayStyle: .enum ) - 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 - } +extension AlertState: Equatable where Action: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.title == rhs.title + && lhs.message == rhs.message + && lhs.buttons == rhs.buttons } +} + +extension AlertState: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.title) + hasher.combine(self.message) + hasher.combine(self.buttons) + } +} + +extension AlertState: Identifiable {} - @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) +extension AlertState.ButtonAction: Equatable where Action: Equatable {} +extension AlertState.ButtonAction.ActionType: Equatable where Action: Equatable {} +extension AlertState.ButtonRole: Equatable {} +extension AlertState.Button: Equatable where Action: Equatable {} + +extension AlertState.ButtonAction: Hashable where Action: Hashable {} +extension AlertState.ButtonAction.ActionType: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + switch self { + case let .send(action), let .animatedSend(action, animation: _): + hasher.combine(action) } } +} +extension AlertState.ButtonRole: Hashable {} +extension AlertState.Button: Hashable where Action: Hashable { + public func hash(into hasher: inout Hasher) { + hasher.combine(self.action) + hasher.combine(self.label) + hasher.combine(self.role) + } +} - @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) +extension AlertState.Button { + func toSwiftUIAction(send: @escaping (Action) -> Void) -> () -> Void { + return { + 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) } } } } - @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) + + func toSwiftUIAlertButton(send: @escaping (Action) -> Void) -> SwiftUI.Alert.Button { + let action = self.toSwiftUIAction(send: send) + switch self.role { + case .cancel: + return .cancel(Text(label), action: action) + case .destructive: + return .destructive(Text(label), action: action) + case .none: + return .default(Text(label), action: action) } } - @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) + #if compiler(>=5.5) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + func toSwiftUIButton(send: @escaping (Action) -> Void) -> some View { + SwiftUI.Button( + role: self.role?.toSwiftUI, + action: self.toSwiftUIAction(send: send) + ) { + Text(self.label) } } - } + #endif +} - @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) - ) +extension AlertState { + #if compiler(>=5.5) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + @ViewBuilder + fileprivate func toSwiftUIActions(send: @escaping (Action) -> Void) -> some View { + ForEach(self.buttons.indices, id: \.self) { + self.buttons[$0].toSwiftUIButton(send: send) } } + #endif + + fileprivate func toSwiftUIAlert(send: @escaping (Action) -> Void) -> SwiftUI.Alert { + if self.buttons.count == 2 { + return SwiftUI.Alert( + title: Text(self.title), + message: self.message.map { Text($0) }, + primaryButton: self.buttons[0].toSwiftUIAlertButton(send: send), + secondaryButton: self.buttons[1].toSwiftUIAlertButton(send: send) + ) + } else { + return SwiftUI.Alert( + title: Text(self.title), + message: self.message.map { Text($0) }, + dismissButton: self.buttons.first?.toSwiftUIAlertButton(send: send) + ) + } } -#endif +} diff --git a/Sources/ComposableArchitecture/SwiftUI/Animation.swift b/Sources/ComposableArchitecture/SwiftUI/Animation.swift index d9349ef..0e19eb9 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Animation.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Animation.swift @@ -1,12 +1,14 @@ -#if canImport(SwiftUI) - import 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) - } +extension ViewStore { + /// Sends an action to the store with a given animation. + /// + /// - Parameters: + /// - action: An action. + /// - animation: An animation. + 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 index 04ad13a..871f9ab 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Binding.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Binding.swift @@ -1,20 +1,194 @@ import CustomDump import SwiftUI +// NB: `BindableAction` can produce crashes in Xcode 12.4 (Swift 5.3) and earlier due to an enum +// protocol witness bug: https://bugs.swift.org/browse/SR-14041 #if compiler(>=5.4) + /// A property wrapper type that can designate properties of app state that can be directly + /// bindable in SwiftUI views. + /// + /// Along with an action type that conforms to the ``BindableAction`` protocol, this type can be + /// used to safely eliminate the boilerplate that is typically incurred when working with multiple + /// mutable fields on state. + /// + /// For example, a settings screen may model its state with the following struct: + /// + /// ```swift + /// struct SettingsState { + /// var digest = Digest.daily + /// var displayName = "" + /// var enableNotifications = false + /// var isLoading = false + /// var protectMyPosts = false + /// var sendEmailNotifications = false + /// var sendMobileNotifications = false + /// } + /// ``` + /// + /// The majority of these fields should be editable by the view, and in the Composable + /// Architecture this means that each field requires a corresponding action that can be sent to + /// the store. Typically this comes in the form of an enum with a case per field: + /// + /// ```swift + /// enum SettingsAction { + /// case digestChanged(Digest) + /// case displayNameChanged(String) + /// case enableNotificationsChanged(Bool) + /// case protectMyPostsChanged(Bool) + /// case sendEmailNotificationsChanged(Bool) + /// case sendMobileNotificationsChanged(Bool) + /// } + /// ``` + /// + /// And we're not even done yet. In the reducer we must now handle each action, which simply + /// replaces the state at each field with a new value: + /// + /// ```swift + /// let settingsReducer = Reducer< + /// SettingsState, SettingsAction, SettingsEnvironment + /// > { state, action, environment in + /// switch action { + /// case let digestChanged(digest): + /// state.digest = digest + /// return .none + /// + /// case let displayNameChanged(displayName): + /// state.displayName = displayName + /// return .none + /// + /// case let enableNotificationsChanged(isOn): + /// state.enableNotifications = isOn + /// return .none + /// + /// case let protectMyPostsChanged(isOn): + /// state.protectMyPosts = isOn + /// return .none + /// + /// case let sendEmailNotificationsChanged(isOn): + /// state.sendEmailNotifications = isOn + /// return .none + /// + /// case let sendMobileNotificationsChanged(isOn): + /// state.sendMobileNotifications = isOn + /// return .none + /// } + /// } + /// ``` + /// + /// This is a _lot_ of boilerplate for something that should be simple. Luckily, we can + /// dramatically eliminate this boilerplate using `BindableState` and ``BindableAction``. + /// + /// First, we can annotate each bindable value of state with the `@BindableState` property + /// wrapper: + /// + /// ```swift + /// struct SettingsState { + /// @BindableState var digest = Digest.daily + /// @BindableState var displayName = "" + /// @BindableState var enableNotifications = false + /// var isLoading = false + /// @BindableState var protectMyPosts = false + /// @BindableState var sendEmailNotifications = false + /// @BindableState var sendMobileNotifications = false + /// } + /// ``` + /// + /// Each annotated field is directly to bindable to SwiftUI controls, like pickers, toggles, and + /// text fields. Notably, the `isLoading` property is _not_ annotated as being bindable, which + /// prevents the view from mutating this value directly. + /// + /// Next, we can conform the action type to ``BindableAction`` by collapsing all of the + /// individual, field-mutating actions into a single case that holds a ``BindingAction`` generic + /// over the reducer's `SettingsState`: + /// + /// ```swift + /// enum SettingsAction: BindableAction { + /// case binding(BindingAction) + /// } + /// ``` + /// + /// And then, we can simplify the settings reducer by allowing the `binding` method to handle + /// these field mutations for us: + /// + /// ```swift + /// let settingsReducer = Reducer< + /// SettingsState, SettingsAction, SettingsEnvironment + /// > { + /// switch action { + /// case .binding: + /// return .none + /// } + /// } + /// .binding() + /// ``` + /// + /// Binding actions are constructed and sent to the store by calling ``ViewStore/binding(_:)`` + /// with a key path to the bindable state: + /// + /// ```swift + /// TextField("Display name", text: viewStore.binding(\.$displayName)) + /// ``` + /// + /// Should you need to layer additional functionality over these bindings, your reducer can + /// pattern match the action for a given key path: + /// + /// ```swift + /// case .binding(\.$displayName): + /// // Validate display name + /// + /// case .binding(\.$enableNotifications): + /// // Return an authorization request effect + /// ``` + /// + /// Binding actions can also be tested in much the same way regular actions are tested. Rather + /// than send a specific action describing how a binding changed, such as + /// `.displayNameChanged("Blob")`, you will send a ``Reducer/binding(action:)`` action that + /// describes which key path is being set to what value, such as `.set(\.$displayName, "Blob")`: + /// + /// ```swift + /// let store = TestStore( + /// initialState: SettingsState(), + /// reducer: settingsReducer, + /// environment: SettingsEnvironment(...) + /// ) + /// + /// store.send(.set(\.$displayName, "Blob")) { + /// $0.displayName = "Blob" + /// } + /// store.send(.set(\.$protectMyPosts, true)) { + /// $0.protectMyPosts = true + /// ) + /// ``` @dynamicMemberLookup @propertyWrapper public struct BindableState { + /// The underlying value wrapped by the bindable state. public var wrappedValue: Value + + /// Creates bindable state from the value of another bindable state. public init(wrappedValue: Value) { self.wrappedValue = wrappedValue } + /// A projection that can be used to derive bindings from a view store. + /// + /// Use the projected value to derive bindings from a view store with properties annotated with + /// `@BindableState`. To get the `projectedValue`, prefix the property with `$`: + /// + /// ```swift + /// TextField("Display name", text: viewStore.binding(\.$displayName)) + /// ``` + /// + /// See ``BindableState`` for more details. public var projectedValue: Self { get { self } set { self = newValue } } + /// Returns bindable state to the resulting value of a given key path. + /// + /// - Parameter keyPath: A key path to a specific resulting value. + /// - Returns: A new bindable state. public subscript( dynamicMember keyPath: WritableKeyPath ) -> BindableState { @@ -67,15 +241,28 @@ import SwiftUI } } + /// An action type that exposes a `binding` case that holds a ``BindingAction``. + /// + /// Used in conjunction with ``BindableState`` to safely eliminate the boilerplate typically + /// associated with mutating multiple fields in state. + /// + /// See the documentation for ``BindableState`` for more details. public protocol BindableAction { - + /// The root state type that contains bindable fields. associatedtype State - + + /// Embeds a binding action in this action type. + /// + /// - Returns: A binding action. static func binding(_ action: BindingAction) -> Self } extension BindableAction { - + /// Constructs a binding action for the given key path and bindable value. + /// + /// Shorthand for `.binding(.set(\.$keyPath, value))`. + /// + /// - Returns: A binding action. public static func set( _ keyPath: WritableKeyPath>, _ value: Value @@ -86,7 +273,10 @@ import SwiftUI } extension ViewStore { - + /// Returns a binding to the resulting bindable state of a given key path. + /// + /// - Parameter keyPath: A key path to a specific bindable state. + /// - Returns: A new binding. public func binding( _ keyPath: WritableKeyPath> ) -> Binding @@ -99,6 +289,12 @@ import SwiftUI } #endif +/// An action that describes simple mutations to some root state at a writable key path. +/// +/// Used in conjunction with ``BindableState`` and ``BindableAction`` to safely eliminate the +/// boilerplate typically associated with mutating multiple fields in state. +/// +/// See the documentation for ``BindableState`` for more details. public struct BindingAction: Equatable { public let keyPath: PartialKeyPath @@ -113,7 +309,15 @@ public struct BindingAction: Equatable { #if compiler(>=5.4) extension BindingAction { - + /// Returns an action that describes simple mutations to some root state at a writable key path + /// to bindable state. + /// + /// - Parameters: + /// - keyPath: A key path to the property that should be mutated. This property must be + /// annotated with the ``BindableState`` property wrapper. + /// - value: A value to assign at the given key path. + /// - Returns: An action that describes simple mutations to some root state at a writable key + /// path. public static func set( _ keyPath: WritableKeyPath>, _ value: Value @@ -126,6 +330,18 @@ public struct BindingAction: Equatable { ) } + /// Matches a binding action by its key path. + /// + /// Implicitly invoked when switching on a reducer's action and pattern matching on a binding + /// action directly to do further work: + /// + /// ```swift + /// case .binding(\.$displayName): // Invokes the `~=` operator. + /// // Validate display name + /// + /// case .binding(\.$enableNotifications): + /// // Return an authorization request effect + /// ``` public static func ~= ( keyPath: WritableKeyPath>, bindingAction: Self @@ -136,7 +352,124 @@ public struct BindingAction: Equatable { #endif extension BindingAction { - + /// Transforms a binding action over some root state to some other type of root state given a + /// key path. + /// + /// Useful in transforming binding actions on view state into binding actions on reducer state + /// when the domain contains ``BindableState`` and ``BindableAction``. + /// + /// For example, we can model an app that can bind an integer count to a stepper and make a + /// network request to fetch a fact about that integer with the following domain: + /// + /// ```swift + /// struct AppState: Equatable { + /// @BindableState var count = 0 + /// var fact: String? + /// ... + /// } + /// + /// enum AppAction: BindableAction { + /// case binding(BindingAction) + /// case factButtonTapped + /// case factResponse(String?) + /// ... + /// } + /// + /// struct AppEnvironment { + /// var numberFact: (Int) -> Effect + /// ... + /// } + /// + /// let appReducer = Reducer { + /// ... + /// } + /// .binding() + /// + /// struct AppView: View { + /// let store: Store + /// + /// var view: some View { + /// ... + /// } + /// } + /// ``` + /// + /// The view may want to limit the state and actions it has access to by introducing a + /// view-specific domain that contains only the state and actions the view needs. Not only will + /// this minimize the number of times a view's `body` is computed, it will prevent the view + /// from accessing state or sending actions outside its purview. We can define it with its own + /// bindable state and bindable action: + /// + /// ```swift + /// extension AppView { + /// struct ViewState: Equatable { + /// @BindableState var count: Int + /// let fact: String? + /// // no access to any other state on `AppState`, like child domains + /// } + /// + /// enum ViewAction: BindableAction { + /// case binding(BindingAction) + /// case factButtonTapped + /// // no access to any other action on `AppAction`, like `factResponse` + /// } + /// } + /// ``` + /// + /// In order to transform a `BindingAction` sent from the view domain into a + /// `BindingAction`, we need a writable key path from `AppState` to `ViewState`. We + /// can synthesize one by defining a computed property on `AppState` with a getter and a setter. + /// The setter should communicate any mutations to bindable state back to the parent state: + /// + /// ```swift + /// extension AppState { + /// var view: AppView.ViewState { + /// get { .init(count: self.count, fact: self.fact) } + /// set { self.count = newValue.count } + /// } + /// } + /// ``` + /// + /// With this property defined it is now possible to transform a `BindingAction` into + /// a `BindingAction`, which means we can transform a `ViewAction` into an + /// `AppAction`. This is where `pullback` comes into play: we can unwrap the view action's + /// binding action on view state and transform it with `pullback` to work with app state. We can + /// define a helper that performs this transformation, as well as route any other view actions + /// to their reducer equivalents: + /// + /// ```swift + /// extension AppAction { + /// static func view(_ viewAction: AppView.ViewAction) -> Self { + /// switch viewAction { + /// case let .binding(action): + /// // transform view binding actions into app binding actions + /// return .binding(action.pullback(\.view)) + /// + /// case let .factButtonTapped + /// // route `ViewAction.factButtonTapped` to `AppAction.factButtonTapped` + /// return .factButtonTapped + /// } + /// } + /// } + /// ``` + /// + /// Finally, in the view we can invoke ``Store/scope(state:action:)`` with these domain + /// transformations to leverage the view store's binding helpers: + /// + /// ```swift + /// WithViewStore( + /// self.store.scope(state: \.view, action: AppAction.view) + /// ) { viewStore in + /// Stepper("\(viewStore.count)", viewStore.binding(\.$count)) + /// Button("Get number fact") { viewStore.send(.factButtonTapped) } + /// if let fact = viewStore.fact { + /// Text(fact) + /// } + /// } + /// ``` + /// + /// - Parameter keyPath: A key path from a new type of root state to the original root state. + /// - Returns: A binding action over a new type of root state. public func pullback( _ keyPath: WritableKeyPath ) -> BindingAction { @@ -163,6 +496,31 @@ extension BindingAction: CustomDumpReflectable { #if compiler(>=5.4) extension Reducer where Action: BindableAction, State == Action.State { + /// Returns a reducer that applies ``BindingAction`` mutations to `State` before running this + /// reducer's logic. + /// + /// For example, a settings screen may gather its binding actions into a single + /// ``BindingAction`` case by conforming to ``BindableAction``: + /// + /// ```swift + /// enum SettingsAction: BindableAction { + /// ... + /// case binding(BindingAction) + /// } + /// ``` + /// + /// The reducer can then be enhanced to automatically handle these mutations for you by tacking + /// on the ``binding()`` method: + /// + /// ```swift + /// let settingsReducer = Reducer { + /// ... + /// } + /// .binding() + /// ``` + /// + /// - Returns: A reducer that applies ``BindingAction`` mutations to `State` before running this + /// reducer's logic. public func binding() -> Self { Self { state, action, environment in guard let bindingAction = (/Action.binding).extract(from: action) diff --git a/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift new file mode 100644 index 0000000..9b12504 --- /dev/null +++ b/Sources/ComposableArchitecture/SwiftUI/ConfirmationDialog.swift @@ -0,0 +1,330 @@ +import CustomDump +import SwiftUI + +/// A data type that describes the state of a confirmation dialog that can be shown to the user. The +/// `Action` generic is the type of actions that can be sent from tapping on a button in the sheet. +/// +/// This type can be used in your application's state in order to control the presentation or +/// dismissal of dialogs. It is preferable to use this API instead of the default SwiftUI API for +/// dialogs because SwiftUI uses 2-way bindings in order to control the showing and dismissal of +/// dialogs, and that does not play nicely with the Composable Architecture. The library requires +/// that all state mutations happen by sending an action so that a reducer can handle that logic, +/// which greatly simplifies how data flows through your application, and gives you instant +/// testability on all parts of your application. +/// +/// To use this API, you model all the dialog actions in your domain's action enum: +/// +/// ```swift +/// enum AppAction: Equatable { +/// case cancelTapped +/// case deleteTapped +/// case favoriteTapped +/// case infoTapped +/// +/// // Your other actions +/// } +/// ``` +/// +/// And you model the state for showing the dialog in your domain's state, and it can start off in a +/// `nil` state: +/// +/// ```swift +/// struct AppState: Equatable { +/// var confirmationDialog: ConfirmationDialogState? +/// +/// // Your other state +/// } +/// ``` +/// +/// Then, in the reducer you can construct an `ConfirmationDialogState` value to represent the +/// dialog you want to show to the user: +/// +/// ```swift +/// let appReducer = Reducer { state, action, env in +/// switch action +/// case .cancelTapped: +/// state.confirmationDialog = nil +/// return .none +/// +/// case .deleteTapped: +/// state.confirmationDialog = nil +/// // Do deletion logic... +/// +/// case .favoriteTapped: +/// state.confirmationDialog = nil +/// // Do favoriting logic +/// +/// case .infoTapped: +/// state.confirmationDialog = .init( +/// title: "What would you like to do?", +/// buttons: [ +/// .default(TextState("Favorite"), action: .send(.favoriteTapped)), +/// .destructive(TextState("Delete"), action: .send(.deleteTapped)), +/// .cancel(), +/// ] +/// ) +/// return .none +/// } +/// } +/// ``` +/// +/// And then, in your view you can use the `confirmationDialog(_:dismiss:)` method on `View` in +/// order to present the dialog in a way that works best with the Composable Architecture: +/// +/// ```swift +/// Button("Info") { viewStore.send(.infoTapped) } +/// .confirmationDialog( +/// self.store.scope(state: \.confirmationDialog), +/// dismiss: .cancelTapped +/// ) +/// ``` +/// +/// This makes your reducer in complete control of when the dialog is shown or dismissed, and makes +/// it so that any choice made in the dialog is automatically fed back into the reducer so that you +/// can handle its logic. +/// +/// Even better, you can instantly write tests that your dialog behavior works as expected: +/// +/// ```swift +/// let store = TestStore( +/// initialState: AppState(), +/// reducer: appReducer, +/// environment: .mock +/// ) +/// +/// store.send(.infoTapped) { +/// $0.confirmationDialog = .init( +/// title: "What would you like to do?", +/// buttons: [ +/// .default(TextState("Favorite"), send: .favoriteTapped), +/// .destructive(TextState("Delete"), send: .deleteTapped), +/// .cancel(), +/// ] +/// ) +/// } +/// store.send(.favoriteTapped) { +/// $0.confirmationDialog = nil +/// // Also verify that favoriting logic executed correctly +/// } +/// ``` +/// +@available(iOS 13, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +public struct ConfirmationDialogState { + public let id = UUID() + public var buttons: [Button] + public var message: TextState? + public var title: TextState + public var titleVisibility: Visibility + + @available(iOS 15, *) + @available(macOS 12, *) + @available(tvOS 15, *) + @available(watchOS 8, *) + public init( + title: TextState, + titleVisibility: Visibility, + message: TextState? = nil, + buttons: [Button] = [] + ) { + self.buttons = buttons + self.message = message + self.title = title + self.titleVisibility = titleVisibility + } + + public init( + title: TextState, + message: TextState? = nil, + buttons: [Button] = [] + ) { + self.buttons = buttons + self.message = message + self.title = title + self.titleVisibility = .automatic + } + + public typealias Button = AlertState.Button + + public enum Visibility { + case automatic + case hidden + case visible + + #if compiler(>=5.5) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + var toSwiftUI: SwiftUI.Visibility { + switch self { + case .automatic: + return .automatic + case .hidden: + return .hidden + case .visible: + return .visible + } + } + #endif + } +} + +@available(iOS 13, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ConfirmationDialogState: CustomDumpReflectable { + public var customDumpMirror: Mirror { + Mirror( + self, + children: [ + "title": self.title, + "message": self.message as Any, + "buttons": self.buttons, + ], + displayStyle: .struct + ) + } +} + +@available(iOS 13, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ConfirmationDialogState: 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, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ConfirmationDialogState: 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, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ConfirmationDialogState: Identifiable {} + +extension View { + /// Displays a dialog when the store's state becomes non-`nil`, and dismisses it when it becomes + /// `nil`. + /// + /// - Parameters: + /// - store: A store that describes if the dialog is shown or dismissed. + /// - dismissal: An action to send when the dialog is dismissed through non-user actions, such + /// as when a dialog is automatically dismissed by the system. Use this action to `nil` out + /// the associated dialog state. + @available(iOS 13, *) + @available(macOS 12, *) + @available(tvOS 13, *) + @available(watchOS 6, *) + @ViewBuilder public func confirmationDialog( + _ store: Store?, Action>, + dismiss: Action + ) -> some View { + #if compiler(>=5.5) + if #available(iOS 15, tvOS 15, watchOS 8, *) { + self.modifier( + NewConfirmationDialogModifier( + viewStore: ViewStore(store, removeDuplicates: { $0?.id == $1?.id }), + dismiss: dismiss + ) + ) + } else { + #if !os(macOS) + self.modifier( + OldConfirmationDialogModifier( + viewStore: ViewStore(store, removeDuplicates: { $0?.id == $1?.id }), + dismiss: dismiss + ) + ) + #endif + } + #elseif !os(macOS) + self.modifier( + OldConfirmationDialogModifier( + viewStore: ViewStore(store, removeDuplicates: { $0?.id == $1?.id }), + dismiss: dismiss + ) + ) + #endif + } +} + +#if compiler(>=5.5) + // NB: Workaround for iOS 14 runtime crashes during iOS 15 availability checks. + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + private struct NewConfirmationDialogModifier: ViewModifier { + @ObservedObject var viewStore: ViewStore?, Action> + let dismiss: Action + + func body(content: Content) -> some View { + content.confirmationDialog( + (viewStore.state?.title).map { Text($0) } ?? Text(""), + isPresented: viewStore.binding(send: dismiss).isPresent(), + titleVisibility: viewStore.state?.titleVisibility.toSwiftUI ?? .automatic, + presenting: viewStore.state, + actions: { $0.toSwiftUIActions(send: viewStore.send) }, + message: { $0.message.map { Text($0) } } + ) + } + } +#endif + +@available(iOS 13, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +private struct OldConfirmationDialogModifier: ViewModifier { + @ObservedObject var viewStore: ViewStore?, Action> + let dismiss: Action + + func body(content: Content) -> some View { + #if !os(macOS) + return content.actionSheet(item: viewStore.binding(send: dismiss)) { state in + state.toSwiftUIActionSheet(send: viewStore.send) + } + #else + return EmptyView() + #endif + } +} + +@available(iOS 13, *) +@available(macOS 12, *) +@available(tvOS 13, *) +@available(watchOS 6, *) +extension ConfirmationDialogState { + #if compiler(>=5.5) + @available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) + @ViewBuilder + fileprivate func toSwiftUIActions(send: @escaping (Action) -> Void) -> some View { + ForEach(self.buttons.indices, id: \.self) { + self.buttons[$0].toSwiftUIButton(send: send) + } + } + #endif + + @available(macOS, unavailable) + fileprivate func toSwiftUIActionSheet(send: @escaping (Action) -> Void) -> SwiftUI.ActionSheet { + SwiftUI.ActionSheet( + title: Text(self.title), + message: self.message.map { Text($0) }, + buttons: self.buttons.map { + $0.toSwiftUIAlertButton(send: send) + } + ) + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift index 8b6fb5f..94710fe 100644 --- a/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift @@ -1,11 +1,86 @@ import OrderedCollections import SwiftUI +/// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with +/// collections of state. +/// +/// ``ForEachStore`` loops over a store's collection with a store scoped to the domain of each +/// element. This allows you to extract and modularize an element's view and avoid concerns around +/// collection index math and parent-child store communication. +/// +/// For example, a todos app may define the domain and logic associated with an individual todo: +/// +/// ```swift +/// struct TodoState: Equatable, Identifiable { +/// let id: UUID +/// var description = "" +/// var isComplete = false +/// } +/// enum TodoAction { +/// case isCompleteToggled(Bool) +/// case descriptionChanged(String) +/// } +/// struct TodoEnvironment {} +/// let todoReducer = Reducer +/// var body: some View { ... } +/// } +/// ``` +/// +/// For a parent domain to work with a collection of todos, it can hold onto this collection in +/// state: +/// +/// ```swift +/// struct AppState: Equatable { +/// var todos: IdentifiedArrayOf = [] +/// } +/// ``` +/// +/// Define a case to handle actions sent to the child domain: +/// +/// ```swift +/// enum AppAction { +/// case todo(id: TodoState.ID, action: TodoAction) +/// } +/// ``` +/// +/// Enhance its reducer using ``Reducer/forEach(state:action:environment:file:line:)-gvte``: +/// +/// ```swift +/// let appReducer = todoReducer.forEach( +/// state: \.todos, +/// action: /AppAction.todo(id:action:), +/// environment: { _ in TodoEnvironment() } +/// ) +/// ``` +/// +/// And finally render a list of `TodoView`s using ``ForEachStore``: +/// +/// ```swift +/// ForEachStore( +/// self.store.scope(state: \.todos, AppAction.todo(id:action:)) +/// ) { todoStore in +/// TodoView(store: todoStore) +/// } +/// ``` +/// public struct ForEachStore: DynamicViewContent where Data: Collection, ID: Hashable, Content: View { public let data: Data let content: () -> Content + /// Initializes a structure that computes views on demand from a store on a collection of data and + /// an identified action. + /// + /// - Parameters: + /// - store: A store on an identified array of data and an identified action. + /// - content: A function that can generate content given a store of an element. public init( _ store: Store, (ID, EachAction)>, @ViewBuilder content: @escaping (Store) -> EachContent @@ -17,11 +92,15 @@ where Data: Collection, ID: Hashable, Content: View { OrderedSet, (ID, EachAction), ForEach, ID, EachContent> > { - self.data = store.state.value + self.data = store.state 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]! + // NB: We cache elements here to avoid a potential crash where SwiftUI may re-evaluate + // views for elements no longer in the collection. + // + // Feedback filed: https://gist.github.com/stephencelis/cdf85ae8dab437adc998fb0204ed9a6b + var element = store.state[id: id]! return content( store.scope( state: { diff --git a/Sources/ComposableArchitecture/SwiftUI/Identified.swift b/Sources/ComposableArchitecture/SwiftUI/Identified.swift index 96034e7..3e50a26 100644 --- a/Sources/ComposableArchitecture/SwiftUI/Identified.swift +++ b/Sources/ComposableArchitecture/SwiftUI/Identified.swift @@ -1,17 +1,44 @@ +/// A wrapper around a value and a hashable identifier that conforms to identifiable. @dynamicMemberLookup public struct Identified: Identifiable where ID: Hashable { public let id: ID public var value: Value + /// Initializes an identified value from a given value and a hashable identifier. + /// + /// - Parameters: + /// - value: A value. + /// - id: A hashable identifier. public init(_ value: Value, id: ID) { self.id = id self.value = value } + /// Initializes an identified value from a given value and a function that can return a hashable + /// identifier from the value. + /// + /// ```swift + /// Identified(uuid, id: \.self) + /// ``` + /// + /// - Parameters: + /// - value: A value. + /// - id: A hashable identifier. public init(_ value: Value, id: (Value) -> ID) { self.init(value, id: id(value)) } + // NB: This overload works around a bug in key path function expressions and `\.self`. + /// Initializes an identified value from a given value and a function that can return a hashable + /// identifier from the value. + /// + /// ```swift + /// Identified(uuid, id: \.self) + /// ``` + /// + /// - Parameters: + /// - value: A value. + /// - id: A key path from the value to a hashable identifier. public init(_ value: Value, id: KeyPath) { self.init(value, id: value[keyPath: id]) } diff --git a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift index 897be22..b69a857 100644 --- a/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift @@ -1,57 +1,102 @@ -#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 +import SwiftUI - 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()) - } - } - } +/// A view that safely unwraps a store of optional state in order to show one of two views. +/// +/// When the underlying state is non-`nil`, the `then` closure will be performed with a ``Store`` +/// that holds onto non-optional state, and otherwise the `else` closure will be performed. +/// +/// This is useful for deciding between two views to show depending on an optional piece of state: +/// +/// ```swift +/// IfLetStore( +/// store.scope(state: \SearchState.results, action: SearchAction.results), +/// then: SearchResultsView.init(store:), +/// else: { Text("Loading search results...") } +/// ) +/// ``` +/// +/// And for performing navigation when a piece of state becomes non-`nil`: +/// +/// ```swift +/// NavigationLink( +/// destination: IfLetStore( +/// self.store.scope(state: \.detail, action: AppAction.detail), +/// then: DetailView.init(store:) +/// ), +/// isActive: viewStore.binding( +/// get: \.isGameActive, +/// send: { $0 ? .startButtonTapped : .detailDismissed } +/// ) +/// ) { +/// Text("Start!") +/// } +/// ``` +/// +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 - ) where Content == IfContent? { - self.store = store - self.content = { viewStore in - if var state = viewStore.state { - return ifContent( + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional + /// state is `nil` or non-`nil`. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil`. + /// - elseContent: A view that is only visible when the optional state is `nil`. + 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 nil - } + ) + } else { + return ViewBuilder.buildEither(second: elseContent()) } } + } - public var body: some View { - WithViewStore( - self.store, - removeDuplicates: { ($0 != nil) == ($1 != nil) }, - content: self.content - ) + /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional + /// state is `nil` or non-`nil`. + /// + /// - Parameters: + /// - store: A store of optional state. + /// - ifContent: A function that is given a store of non-optional state and returns a view that + /// is visible only when the optional state is non-`nil`. + 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 + } } } -#endif + + public var body: some View { + WithViewStore( + self.store, + removeDuplicates: { ($0 != nil) == ($1 != nil) }, + content: self.content + ) + } +} diff --git a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift index dd5f67b..24097bc 100644 --- a/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/SwitchStore.swift @@ -1,220 +1,493 @@ -#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 +import SwiftUI - init( - store: Store, - @ViewBuilder content: @escaping () -> Content - ) { - self.store = store - self.content = content - } +#if DEBUG + import os +#endif - public var body: some View { - self.content() - .environmentObject(StoreObservableObject(store: self.store)) - } +/// A view that can switch over a store of enum state and handle each case. +/// +/// An application may model parts of its state with enums. For example, app state may differ if a +/// user is logged-in or not: +/// +/// ```swift +/// enum AppState { +/// case loggedIn(LoggedInState) +/// case loggedOut(LoggedOutState) +/// } +/// ``` +/// +/// In the view layer, a store on this state can switch over each case using a ``SwitchStore`` and +/// a ``CaseLet`` view per case: +/// +/// ```swift +/// struct AppView: View { +/// let store: Store +/// +/// var body: some View { +/// SwitchStore(self.store) { +/// CaseLet(state: /AppState.loggedIn, action: AppAction.loggedIn) { loggedInStore in +/// LoggedInView(store: loggedInStore) +/// } +/// CaseLet(state: /AppState.loggedOut, action: AppAction.loggedOut) { loggedOutStore in +/// LoggedOutView(store: loggedOutStore) +/// } +/// } +/// } +/// } +/// ``` +/// +/// If a ``SwitchStore`` does not exhaustively handle every case with a corresponding ``CaseLet`` +/// view, a runtime warning will be logged when an unhandled case is encountered. To fall back on a +/// default view instead, introduce a ``Default`` view at the end of the ``SwitchStore``: +/// +/// ```swift +/// SwitchStore(self.store) { +/// CaseLet(state: /MyState.first, action: MyAction.first, then: FirstView.init(store:)) +/// CaseLet(state: /MyState.second, action: MyAction.second, then: SecondView.init(store:)) +/// +/// Default { +/// Text("State is neither first nor second.") +/// } +/// } +/// ``` +/// +/// - See also: ``Reducer/pullback(state:action:environment:file:line:)``, a method that aids in +/// transforming reducers that operate on each case of an enum into reducers that operate on the +/// entire enum. +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 } - @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 var body: some View { + self.content() + .environmentObject(StoreObservableObject(store: self.store)) + } +} - 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 - } +/// A view that handles a specific case of enum state in a ``SwitchStore``. +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 var body: some View { - IfLetStore( - self.store.wrappedValue.scope( - state: self.toLocalState, - action: self.fromLocalAction - ), - then: self.content - ) - } + /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state + /// matches a particular case. + /// + /// - Parameters: + /// - toLocalState: A function that can extract a case of switch store state, which can be + /// specified using case path literal syntax, _e.g._ `/State.case`. + /// - fromLocalAction: A function that can embed a case action in a switch store action. + /// - content: A function that is given a store of the given case's state and returns a view + /// that is visible only when the switch store's state matches. + 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 } - /// A view that covers any cases that aren't addressed in a ``SwitchStore``. + public var body: some View { + IfLetStore( + self.store.wrappedValue.scope( + state: self.toLocalState, + action: self.fromLocalAction + ), + then: self.content + ) + } +} + +extension CaseLet where GlobalAction == LocalAction { + /// Initializes a ``CaseLet`` view that computes content depending on if a store of enum state + /// matches a particular case. + /// + /// - Parameters: + /// - toLocalState: A function that can extract a case of switch store state, which can be + /// specified using case path literal syntax, _e.g._ `/State.case`. + /// - content: A function that is given a store of the given case's state and returns a view + /// that is visible only when the switch store's state matches. + public init( + state toLocalState: @escaping (GlobalState) -> LocalState?, + @ViewBuilder then content: @escaping (Store) -> Content + ) { + self.init( + state: toLocalState, + action: { $0 }, + then: 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. +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. /// - /// 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 + /// - 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 + } - /// 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() + } +} + +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 var body: some View { - self.content() + 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) } } } - @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) - extension SwitchStore { - public init( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( + public init( + _ store: Store, + @ViewBuilder content: @escaping () -> TupleView< + ( + CaseLet, + CaseLet, + Default + ) + > + ) + where + Content == WithViewStore< + State, + Action, + _ConditionalContent< + _ConditionalContent< CaseLet, - Default - ) + CaseLet + >, + Default > - ) - where - Content == WithViewStore< - State, - Action, + > + { + 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 { - content.1 - } + > + { + 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 () -> CaseLet - ) - where - Content == WithViewStore< - State, - Action, + 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> > > - { - self.init(store) { - content() - Default { _ExhaustivityCheckView(file: file, line: line) } - } + > + { + let content = content() + self.init(store) { + content.value.0 + content.value.1 + content.value.2 + Default { _ExhaustivityCheckView(file: file, line: line) } } + } - public init( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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 >, - Default - > + _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 - } + > + { + 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( - _ store: Store, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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 >, - Default<_ExhaustivityCheckView> - > + _ConditionalContent< + CaseLet, + CaseLet + > + >, + Default<_ExhaustivityCheckView> > - { - let content = content() - self.init(store) { - content.value.0 - content.value.1 - Default { _ExhaustivityCheckView(file: file, line: line) } - } + > + { + 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, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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, @@ -222,43 +495,61 @@ >, _ConditionalContent< CaseLet, - Default + 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 - } + > + { + 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( - _ store: Store, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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, @@ -266,263 +557,327 @@ >, _ConditionalContent< CaseLet, - Default<_ExhaustivityCheckView> + 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) } - } + > + { + 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, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > + 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 { - content.4 - } + > + { + 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 - >( - _ store: Store, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > + 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 - Default { _ExhaustivityCheckView(file: file, line: line) } - } + > + { + 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, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > + 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 { - content.5 - } + > + { + 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 - >( - _ store: Store, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< - _ConditionalContent< - CaseLet, - CaseLet - >, - _ConditionalContent< - CaseLet, - CaseLet - > + 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 - Default { _ExhaustivityCheckView(file: file, line: line) } - } + > + { + 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, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< @@ -539,59 +894,73 @@ CaseLet, CaseLet >, - Default + _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 - } + > + { + 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 - >( - _ store: Store, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< @@ -608,51 +977,63 @@ CaseLet, CaseLet >, - Default<_ExhaustivityCheckView> + _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) } - } + > + { + 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, - DefaultContent - >( - _ store: Store, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - Default - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< @@ -671,64 +1052,78 @@ >, _ConditionalContent< CaseLet, - Default + 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 - } + > + { + 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 - >( - _ store: Store, - file: StaticString = #fileID, - line: UInt = #line, - @ViewBuilder content: @escaping () -> TupleView< - ( - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet, - CaseLet - ) - > - ) - where - Content == WithViewStore< - State, - Action, + 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< @@ -747,417 +1142,110 @@ >, _ConditionalContent< CaseLet, - Default<_ExhaustivityCheckView> + CaseLet > > - > - > - { - 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> - > + 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) } - } + > + { + 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 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) + 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. + "\(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 + 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) - --- + Text(message) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .foregroundColor(.white) + .padding() + .background(Color.red.edgesIgnoringSafeArea(.all)) + .onAppear { + #if DEBUG + os_log( + .fault, dso: rw.dso, log: rw.log, """ + SwitchStore@%@:%d does not handle the current case. … + + Unhandled case: + %@ + + Make sure that you exhaustively provide a "CaseLet" view for each case in your state, \ + or provide a "Default" view at the end of the "SwitchStore". + """, + "\(self.file)", + self.line, + debugCaseOutput(self.store.wrappedValue.state) ) - } - #else - return EmptyView() - #endif - } + #endif + } + #else + return EmptyView() + #endif } +} - @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) - private class StoreObservableObject: ObservableObject { - let wrappedValue: Store +private class StoreObservableObject: ObservableObject { + let wrappedValue: Store - init(store: Store) { - self.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 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 +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 +} diff --git a/Sources/ComposableArchitecture/SwiftUI/TextState.swift b/Sources/ComposableArchitecture/SwiftUI/TextState.swift index c108acd..98e46a3 100644 --- a/Sources/ComposableArchitecture/SwiftUI/TextState.swift +++ b/Sources/ComposableArchitecture/SwiftUI/TextState.swift @@ -1,337 +1,503 @@ -#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?) - } +import CustomDump +import SwiftUI + +/// An equatable description of SwiftUI `Text`. Useful for storing rich text in state for the +/// purpose of rendering in a view hierarchy. +/// +/// Although `SwiftUI.Text` and `SwiftUI.LocalizedStringKey` are value types that conform to +/// `Equatable`, their `==` do not return `true` when used with seemingly equal values. If we were +/// to naively store these values in state, our tests may begin to fail. +/// +/// ``TextState`` solves this problem by providing an interface similar to `SwiftUI.Text` that can +/// be held in state and asserted against. +/// +/// Let's say you wanted to hold some dynamic, styled text content in your app state. You could use +/// ``TextState``: +/// +/// ```swift +/// struct AppState: Equatable { +/// var label: TextState +/// } +/// ``` +/// +/// Your reducer can then assign a value to this state using an API similar to that of +/// `SwiftUI.Text`. +/// +/// ```swift +/// state.label = TextState("Hello, ") + TextState(name).bold() + TextState("!") +/// ``` +/// +/// And your view store can render it directly: +/// +/// ```swift +/// var body: some View { +/// WithViewStore(self.store) { viewStore in +/// viewStore.label +/// } +/// } +/// ``` +/// +/// Certain SwiftUI APIs, like alerts and confirmation dialogs, take `Text` values and, not views. +/// To convert ``TextState`` to `SwiftUI.Text` for this purpose, you can use the `Text` initializer: +/// +/// ```swift +/// Alert(title: Text(viewStore.label)) +/// ``` +/// +/// The Composable Architecture comes with a few convenience APIs for alerts and dialogs that wrap +/// ``TextState`` under the hood. See ``AlertState`` and `ActionState` accordingly. +/// +/// In the future, should `SwiftUI.Text` and `SwiftUI.LocalizedStringKey` reliably conform to +/// `Equatable`, ``TextState`` may be deprecated. +/// +/// - Note: ``TextState`` does not support _all_ `LocalizedStringKey` permutations at this time +/// (interpolated `SwiftUI.Image`s, for example). ``TextState`` also uses reflection to determine +/// `LocalizedStringKey` equatability, so be mindful of edge cases. +public struct TextState: Equatable, Hashable { + fileprivate var modifiers: [Modifier] = [] + fileprivate let storage: Storage + + fileprivate enum Modifier: Equatable, Hashable { + case accessibilityHeading(AccessibilityHeadingLevel) + case accessibilityLabel(TextState) + case accessibilityTextContentType(AccessibilityTextContentType) + 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) + 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 + 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 (.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 (.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 + 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 - } + // 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 - } + 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) - } + 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) - } +// MARK: - API - @_disfavoredOverload - public init(_ content: S) where S: StringProtocol { - self.init(verbatim: String(content)) - } +extension TextState { + public init(verbatim content: String) { + self.storage = .verbatim(content) + } - public init( - _ key: LocalizedStringKey, - tableName: String? = nil, - bundle: Bundle? = nil, - comment: StaticString? = nil - ) { - self.storage = .localized(key, tableName: tableName, bundle: bundle, comment: comment) - } + @_disfavoredOverload + public init(_ content: S) where S: StringProtocol { + self.init(verbatim: String(content)) + } - public static func + (lhs: Self, rhs: Self) -> Self { - .init(storage: .concatenated(lhs, rhs)) - } + public init( + _ key: LocalizedStringKey, + tableName: String? = nil, + bundle: Bundle? = nil, + comment: StaticString? = nil + ) { + self.storage = .localized(key, tableName: tableName, bundle: bundle, comment: comment) + } - public func baselineOffset(_ baselineOffset: CGFloat) -> Self { - var `self` = self - `self`.modifiers.append(.baselineOffset(baselineOffset)) - return `self` - } + public static func + (lhs: Self, rhs: Self) -> Self { + .init(storage: .concatenated(lhs, rhs)) + } - public func bold() -> Self { - var `self` = self - `self`.modifiers.append(.bold) - return `self` - } + public func baselineOffset(_ baselineOffset: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.baselineOffset(baselineOffset)) + return `self` + } - public func font(_ font: Font?) -> Self { - var `self` = self - `self`.modifiers.append(.font(font)) - return `self` - } + public func bold() -> Self { + var `self` = self + `self`.modifiers.append(.bold) + return `self` + } - public func fontWeight(_ weight: Font.Weight?) -> Self { - var `self` = self - `self`.modifiers.append(.fontWeight(weight)) - return `self` - } + public func font(_ font: Font?) -> Self { + var `self` = self + `self`.modifiers.append(.font(font)) + return `self` + } - public func foregroundColor(_ color: Color?) -> Self { - var `self` = self - `self`.modifiers.append(.foregroundColor(color)) - return `self` - } + public func fontWeight(_ weight: Font.Weight?) -> Self { + var `self` = self + `self`.modifiers.append(.fontWeight(weight)) + return `self` + } - public func italic() -> Self { - var `self` = self - `self`.modifiers.append(.italic) - return `self` - } + public func foregroundColor(_ color: Color?) -> Self { + var `self` = self + `self`.modifiers.append(.foregroundColor(color)) + return `self` + } - public func kerning(_ kerning: CGFloat) -> Self { - var `self` = self - `self`.modifiers.append(.kerning(kerning)) - return `self` - } + public func italic() -> Self { + var `self` = self + `self`.modifiers.append(.italic) + 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 kerning(_ kerning: CGFloat) -> Self { + var `self` = self + `self`.modifiers.append(.kerning(kerning)) + return `self` + } - public func tracking(_ tracking: CGFloat) -> Self { - var `self` = self - `self`.modifiers.append(.tracking(tracking)) - 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 underline(_ active: Bool = true, color: Color? = nil) -> Self { - var `self` = self - `self`.modifiers.append(.underline(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` } +} + +// MARK: Accessibility + +extension TextState { + public enum AccessibilityTextContentType: String, Equatable, Hashable { + case console, fileSystem, messaging, narrative, plain, sourceCode, spreadsheet, wordProcessing - @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) + #if compiler(>=5.5.1) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + var toSwiftUI: SwiftUI.AccessibilityTextContentType { + switch self { + case .console: return .console + case .fileSystem: return .fileSystem + case .messaging: return .messaging + case .narrative: return .narrative + case .plain: return .plain + case .sourceCode: return .sourceCode + case .spreadsheet: return .spreadsheet + case .wordProcessing: return .wordProcessing + } } - 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) + #endif + } + + public enum AccessibilityHeadingLevel: String, Equatable, Hashable { + case h1, h2, h3, h4, h5, h6, unspecified + + #if compiler(>=5.5.1) + @available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) + var toSwiftUI: SwiftUI.AccessibilityHeadingLevel { + switch self { + case .h1: return .h1 + case .h2: return .h2 + case .h3: return .h3 + case .h4: return .h4 + case .h5: return .h5 + case .h6: return .h6 + case .unspecified: return .unspecified } } - } + #endif + } +} + +@available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) +extension TextState { + public func accessibilityHeading(_ headingLevel: AccessibilityHeadingLevel) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityHeading(headingLevel)) + return `self` } - @available(iOS 13, macOS 10.15, macCatalyst 13, tvOS 13, watchOS 6, *) - extension TextState: View { - public var body: some View { - Text(self) - } + public func accessibilityLabel(_ string: String) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(.init(string))) + return `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) + public func accessibilityLabel(_ string: S) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityLabel(.init(string))) + return `self` + } - case let .localized(key, tableName, bundle, comment): - self = key.formatted( - locale: locale, - tableName: tableName, - bundle: bundle, - comment: comment - ) + public func accessibilityLabel( + _ key: LocalizedStringKey, tableName: String? = nil, bundle: Bundle? = nil, + comment: StaticString? = nil + ) -> Self { + var `self` = self + `self`.modifiers.append( + .accessibilityLabel(.init(key, tableName: tableName, bundle: bundle, comment: comment))) + return `self` + } - case let .verbatim(string): - self = string + public func accessibilityTextContentType(_ type: AccessibilityTextContentType) -> Self { + var `self` = self + `self`.modifiers.append(.accessibilityTextContentType(type)) + return `self` + } +} + +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 { + #if compiler(>=5.5.1) + case let .accessibilityHeading(level): + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + return text.accessibilityHeading(level.toSwiftUI) + } else { + return text + } + case let .accessibilityLabel(value): + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + switch value.storage { + case let .verbatim(string): + return text.accessibilityLabel(string) + case let .localized(key, tableName, bundle, comment): + return text.accessibilityLabel( + Text(key, tableName: tableName, bundle: bundle, comment: comment)) + case .concatenated(_, _): + assertionFailure("`.accessibilityLabel` does not support contcatenated `TextState`") + return text + } + } else { + return text + } + case let .accessibilityTextContentType(type): + if #available(iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0, *) { + return text.accessibilityTextContentType(type.toSwiftUI) + } else { + return text + } + #else + case .accessibilityHeading, + .accessibilityLabel, + .accessibilityTextContentType: + return text + #endif + 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 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 - } +extension TextState: View { + public var body: some View { + Text(self) + } +} + +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) - let format = NSLocalizedString( - key, + case let .localized(key, tableName, bundle, comment): + self = key.formatted( + locale: locale, tableName: tableName, - bundle: bundle ?? .main, - value: "", - comment: comment.map(String.init) ?? "" + bundle: bundle, + comment: comment ) - return String(format: format, locale: locale, arguments: arguments) - } - public var debugOutput: String { - self.formatted().debugDescription + case let .verbatim(string): + self = string } } - - @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 +} + +extension LocalizedStringKey { + // 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. + fileprivate 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 } - 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)" - } + 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) + } +} + +// MARK: - CustomDumpRepresentable + +extension TextState: CustomDumpRepresentable { + public var customDumpValue: Any { + func dumpHelp(_ textState: Self) -> String { + var output: String + switch textState.storage { + case let .concatenated(lhs, rhs): + output = dumpHelp(lhs) + dumpHelp(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 .accessibilityHeading(headingLevel): + let tag = "accessibility-heading-level" + output = "<\(tag)=\(headingLevel.rawValue)>\(output)" + case let .accessibilityLabel(value): + let tag = "accessibility-label" + output = "<\(tag)=\(dumpHelp(value))>\(output)" + case let .accessibilityTextContentType(type): + let tag = "accessibility-text-content-type" + output = "<\(tag)=\(type.rawValue)>\(output)" + 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 } + 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)) - ) - """# + return output } + + return dumpHelp(self) } -#endif +} diff --git a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift index a795b71..f5f777a 100644 --- a/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift +++ b/Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift @@ -1,18 +1,33 @@ -#if canImport(Combine) && canImport(SwiftUI) import Combine +import CustomDump import SwiftUI -@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +/// A structure that transforms a store into an observable view store in order to compute views from +/// store state. +/// +/// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core +/// views provided by SwiftUI. The known problematic views are: +/// +/// * If a `GeometryReader` or `ScrollViewReader` is used inside a ``WithViewStore`` it will not +/// receive state updates correctly. To work around you either need to reorder the views so that +/// the `GeometryReader` or `ScrollViewReader` wraps ``WithViewStore``, or, if that is not +/// possible, then you must hold onto an explicit +/// `@ObservedObject var viewStore: ViewStore` in your view in lieu of using this +/// helper (see [here](https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18)). +/// * If you create a `Stepper` via the `Stepper.init(onIncrement:onDecrement:label:)` initializer +/// inside a ``WithViewStore`` it will behave erratically. To work around you should use the +/// initializer that takes a binding (see +/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)). 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 + #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, @@ -21,58 +36,71 @@ public struct WithViewStore { 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 + #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) } - + + /// Prints debug information to the console whenever the view is computed. + /// + /// - Parameter prefix: A string with which to prefix all debug messages. + /// - Returns: A structure that prints debug messages for all computations. public func debug(_ prefix: String = "") -> Self { var view = self -#if DEBUG - view.prefix = prefix -#endif + #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) + #if DEBUG + if let prefix = self.prefix { + var stateDump = "" + customDump(self.viewStore.state, to: &stateDump, indent: 2) + let difference = + self.previousState(self.viewStore.state) + .map { + diff($0, self.viewStore.state).map { "(Changed state)\n\($0)" } + ?? "(No difference in state detected)" + } + ?? "(Initial state)\n\(stateDump)" + func typeName(_ type: Any.Type) -> String { + var name = String(reflecting: type) + if let index = name.firstIndex(of: ".") { + name.removeSubrange(...index) + } + return name } - return name + print( + """ + \(prefix.isEmpty ? "" : "\(prefix): ")\ + WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\ + @\(self.file):\(self.line) \(difference) + """ + ) } - print( - """ - \(prefix.isEmpty ? "" : "\(prefix): ")\ - WithViewStore<\(typeName(State.self)), \(typeName(Action.self)), _>\ - @\(self.file):\(self.line) \(difference) - """ - ) - } -#endif + #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 { + /// Initializes a structure that transforms a store into an observable view store in order to + /// compute views from store state. + /// + /// - Parameters: + /// - store: A store. + /// - isDuplicate: A function to determine when two `State` values are equal. When values are + /// equal, repeat view computations are removed, + /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, @@ -88,14 +116,19 @@ extension WithViewStore: View where Content: View { 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 { + /// Initializes a structure that transforms a store into an observable view store in order to + /// compute views from equatable store state. + /// + /// - Parameters: + /// - store: A store of equatable state. + /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, @@ -106,8 +139,13 @@ extension WithViewStore where State: Equatable, Content: View { } } -@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) extension WithViewStore where State == Void, Content: View { + /// Initializes a structure that transforms a store into an observable view store in order to + /// compute views from equatable store state. + /// + /// - Parameters: + /// - store: A store of equatable state. + /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, @@ -118,21 +156,24 @@ extension WithViewStore where State == Void, Content: View { } } -@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 { + /// Initializes a structure that transforms a store into an observable view store in order to + /// compute views from store state. + /// + /// - Parameters: + /// - store: A store. + /// - isDuplicate: A function to determine when two `State` values are equal. When values are + /// equal, repeat view computations are removed, + /// - content: A function that can generate content from a view store. public init( _ store: Store, removeDuplicates isDuplicate: @escaping (State, State) -> Bool, @@ -148,7 +189,7 @@ extension WithViewStore: Scene where Content: Scene { content: content ) } - + public var body: Content { self._body } @@ -156,6 +197,12 @@ extension WithViewStore: Scene where Content: Scene { @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) extension WithViewStore where State: Equatable, Content: Scene { + /// Initializes a structure that transforms a store into an observable view store in order to + /// compute scenes from equatable store state. + /// + /// - Parameters: + /// - store: A store of equatable state. + /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, @@ -168,6 +215,12 @@ extension WithViewStore where State: Equatable, Content: Scene { @available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) extension WithViewStore where State == Void, Content: Scene { + /// Initializes a structure that transforms a store into an observable view store in order to + /// compute scenes from equatable store state. + /// + /// - Parameters: + /// - store: A store of equatable state. + /// - content: A function that can generate content from a view store. public init( _ store: Store, file: StaticString = #fileID, @@ -177,5 +230,3 @@ extension WithViewStore where State == Void, Content: Scene { self.init(store, removeDuplicates: ==, file: file, line: line, content: content) } } - -#endif diff --git a/Sources/ComposableArchitecture/TestSupport/FailingEffect.swift b/Sources/ComposableArchitecture/TestSupport/FailingEffect.swift new file mode 100644 index 0000000..715700e --- /dev/null +++ b/Sources/ComposableArchitecture/TestSupport/FailingEffect.swift @@ -0,0 +1,89 @@ +import XCTestDynamicOverlay + +extension Effect { + /// An effect that causes a test to fail if it runs. + /// + /// This effect can provide an additional layer of certainty that a tested code path does not + /// execute a particular effect. + /// + /// For example, let's say we have a very simple counter application, where a user can increment + /// and decrement a number. The state and actions are simple enough: + /// + /// ```swift + /// struct CounterState: Equatable { + /// var count = 0 + /// } + /// + /// enum CounterAction: Equatable { + /// case decrementButtonTapped + /// case incrementButtonTapped + /// } + /// ``` + /// + /// Let's throw in a side effect. If the user attempts to decrement the counter below zero, the + /// application should refuse and play an alert sound instead. + /// + /// We can model playing a sound in the environment with an effect: + /// + /// ```swift + /// struct CounterEnvironment { + /// let playAlertSound: () -> Effect + /// } + /// ``` + /// + /// Now that we've defined the domain, we can describe the logic in a reducer: + /// + /// ```swift + /// let counterReducer = Reducer< + /// CounterState, CounterAction, CounterEnvironment + /// > { state, action, environment in + /// switch action { + /// case .decrementButtonTapped: + /// if state > 0 { + /// state.count -= 0 + /// return .none + /// } else { + /// return environment.playAlertSound() + /// .fireAndForget() + /// } + /// + /// case .incrementButtonTapped: + /// state.count += 1 + /// return .non + /// } + /// } + /// ``` + /// + /// Let's say we want to write a test for the increment path. We can see in the reducer that it + /// should never play an alert, so we can configure the environment with an effect that will + /// fail if it ever executes: + /// + /// ```swift + /// func testIncrement() { + /// let store = TestStore( + /// initialState: CounterState(count: 0) + /// reducer: counterReducer, + /// environment: CounterEnvironment( + /// playSound: .failing("playSound") + /// ) + /// ) + /// + /// store.send(.increment) { + /// $0.count = 1 + /// } + /// } + /// ``` + /// + /// By using a `.failing` effect in our environment we have strengthened the assertion and made + /// the test easier to understand at the same time. We can see, without consulting the reducer + /// itself, that this particular action should not access this effect. + /// + /// - Parameter prefix: A string that identifies this scheduler and will prefix all failure + /// messages. + /// - Returns: An effect that causes a test to fail if it runs. + public static func failing(_ prefix: String) -> Self { + .fireAndForget { + XCTFail("\(prefix.isEmpty ? "" : "\(prefix) - ")A failing effect ran.") + } + } +} diff --git a/Sources/ComposableArchitecture/TestSupport/TestStore.swift b/Sources/ComposableArchitecture/TestSupport/TestStore.swift new file mode 100644 index 0000000..2d1fa3c --- /dev/null +++ b/Sources/ComposableArchitecture/TestSupport/TestStore.swift @@ -0,0 +1,493 @@ +#if DEBUG + import Combine + import CustomDump + import Foundation + import XCTestDynamicOverlay + + /// A testable runtime for a reducer. + /// + /// This object aids in writing expressive and exhaustive tests for features built in the + /// Composable Architecture. It allows you to send a sequence of actions to the store, and each + /// step of the way you must assert exactly how state changed, and how effect emissions were fed + /// back into the system. + /// + /// There are multiple ways the test store forces you to exhaustively assert on how your feature + /// behaves: + /// + /// * After each action is sent you must describe precisely how the state changed from before + /// the action was sent to after it was sent. + /// + /// If even the smallest piece of data differs the test will fail. This guarantees that you + /// are proving you know precisely how the state of the system changes. + /// + /// * Sending an action can sometimes cause an effect to be executed, and if that effect emits + /// an action that is fed back into the system, you **must** explicitly assert that you expect + /// to receive that action from the effect, _and_ you must assert how state changed as a + /// result. + /// + /// If you try to send another action before you have handled all effect emissions the + /// assertion will fail. This guarantees that you do not accidentally forget about an effect + /// emission, and that the sequence of steps you are describing will mimic how the application + /// behaves in reality. + /// + /// * All effects must complete by the time the assertion has finished running the steps you + /// specify. + /// + /// If at the end of the assertion there is still an in-flight effect running, the assertion + /// will fail. This helps exhaustively prove that you know what effects are in flight and + /// forces you to prove that effects will not cause any future changes to your state. + /// + /// For example, given a simple counter reducer: + /// + /// ```swift + /// struct CounterState { + /// var count = 0 + /// } + /// + /// enum CounterAction: Equatable { + /// case decrementButtonTapped + /// case incrementButtonTapped + /// } + /// + /// let counterReducer = Reducer { state, action, _ in + /// switch action { + /// case .decrementButtonTapped: + /// state.count -= 1 + /// return .none + /// + /// case .incrementButtonTapped: + /// state.count += 1 + /// return .none + /// } + /// } + /// ``` + /// + /// One can assert against its behavior over time: + /// + /// ```swift + /// class CounterTests: XCTestCase { + /// func testCounter() { + /// let store = TestStore( + /// initialState: .init(count: 0), // GIVEN counter state of 0 + /// reducer: counterReducer, + /// environment: () + /// ) + /// store.send(.incrementButtonTapped) { // WHEN the increment button is tapped + /// $0.count = 1 // THEN the count should be 1 + /// } + /// } + /// } + /// ``` + /// + /// Note that in the trailing closure of `.send(.incrementButtonTapped)` we are given a single + /// mutable value of the state before the action was sent, and it is our job to mutate the value + /// to match the state after the action was sent. In this case the `count` field changes to `1`. + /// + /// For a more complex example, consider the following bare-bones search feature that uses the + /// ``Effect/debounce(id:for:scheduler:options:)`` operator to wait for the user to stop typing + /// before making a network request: + /// + /// ```swift + /// struct SearchState: Equatable { + /// var query = "" + /// var results: [String] = [] + /// } + /// + /// enum SearchAction: Equatable { + /// case queryChanged(String) + /// case response([String]) + /// } + /// + /// struct SearchEnvironment { + /// var mainQueue: AnySchedulerOf + /// var request: (String) -> Effect<[String], Never> + /// } + /// + /// let searchReducer = Reducer { + /// state, action, environment in + /// + /// struct SearchId: Hashable {} + /// + /// switch action { + /// case let .queryChanged(query): + /// state.query = query + /// return environment.request(self.query) + /// .debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue) + /// + /// case let .response(results): + /// state.results = results + /// return .none + /// } + /// } + /// ``` + /// + /// It can be fully tested by controlling the environment's scheduler and effect: + /// + /// ```swift + /// // Create a test dispatch scheduler to control the timing of effects + /// let scheduler = DispatchQueue.test + /// + /// let store = TestStore( + /// initialState: SearchState(), + /// reducer: searchReducer, + /// environment: SearchEnvironment( + /// // Wrap the test scheduler in a type-erased scheduler + /// mainQueue: scheduler.eraseToAnyScheduler(), + /// // Simulate a search response with one item + /// request: { _ in Effect(value: ["Composable Architecture"]) } + /// ) + /// ) + /// + /// // Change the query + /// store.send(.searchFieldChanged("c") { + /// // Assert that state updates accordingly + /// $0.query = "c" + /// } + /// + /// // Advance the scheduler by a period shorter than the debounce + /// scheduler.advance(by: 0.25) + /// + /// // Change the query again + /// store.send(.searchFieldChanged("co") { + /// $0.query = "co" + /// } + /// + /// // Advance the scheduler by a period shorter than the debounce + /// scheduler.advance(by: 0.25) + /// // Advance the scheduler to the debounce + /// scheduler.advance(by: 0.25) + /// + /// // Assert that the expected response is received + /// store.receive(.response(["Composable Architecture"])) { + /// // Assert that state updates accordingly + /// $0.results = ["Composable Architecture"] + /// } + /// ``` + /// + /// This test is proving that the debounced network requests are correctly canceled when we do not + /// wait longer than the 0.5 seconds, because if it wasn't and it delivered an action when we did + /// not expect it would cause a test failure. + /// + public final class TestStore { + public var environment: Environment + + private let file: StaticString + private let fromLocalAction: (LocalAction) -> Action + private var line: UInt + private var longLivingEffects: Set = [] + var receivedActions: [(action: Action, state: State)] = [] + private let reducer: Reducer + private var snapshotState: State + private var store: Store! + private let toLocalState: (State) -> LocalState + + private init( + environment: Environment, + file: StaticString, + fromLocalAction: @escaping (LocalAction) -> Action, + initialState: State, + line: UInt, + reducer: Reducer, + toLocalState: @escaping (State) -> LocalState + ) { + self.environment = environment + self.file = file + self.fromLocalAction = fromLocalAction + self.line = line + self.reducer = reducer + self.snapshotState = initialState + self.toLocalState = toLocalState + + self.store = Store( + initialState: initialState, + reducer: Reducer { [unowned self] state, action, _ in + let effects: Effect + switch action.origin { + case let .send(localAction): + effects = self.reducer.run(&state, self.fromLocalAction(localAction), self.environment) + self.snapshotState = state + + case let .receive(action): + effects = self.reducer.run(&state, action, self.environment) + self.receivedActions.append((action, state)) + } + + let effect = LongLivingEffect(file: action.file, line: action.line) + return + effects + .on( + starting: { [weak self] in self?.longLivingEffects.insert(effect) }, + completed: { [weak self] in self?.longLivingEffects.remove(effect) }, + disposed: { [weak self] in self?.longLivingEffects.remove(effect) } + ) + .map { .init(origin: .receive($0), file: action.file, line: action.line) } + .eraseToEffect() + + }, + environment: () + ) + } + + deinit { + self.completed() + } + + func completed() { + if !self.receivedActions.isEmpty { + var actions = "" + customDump(self.receivedActions.map(\.action), to: &actions) + XCTFail( + """ + The store received \(self.receivedActions.count) unexpected \ + action\(self.receivedActions.count == 1 ? "" : "s") after this one: … + + Unhandled actions: \(actions) + """, + file: self.file, line: self.line + ) + } + for effect in self.longLivingEffects { + XCTFail( + """ + An effect returned for this action is still running. It must complete before the end of \ + the test. … + + To fix, inspect any effects the reducer returns for this action and ensure that all of \ + them complete by the end of the test. There are a few reasons why an effect may not have \ + completed: + + • If an effect uses a scheduler (via "receive(on:)", "delay", "debounce", etc.), make \ + sure that you wait enough time for the scheduler to perform the effect. If you are using \ + a test scheduler, advance the scheduler so that the effects may complete, or consider \ + using an immediate scheduler to immediately perform the effect instead. + + • If you are returning a long-living effect (timers, notifications, subjects, etc.), \ + then make sure those effects are torn down by marking the effect ".cancellable" and \ + returning a corresponding cancellation effect ("Effect.cancel") from another action, or, \ + if your effect is driven by a Combine subject, send it a completion. + """, + file: effect.file, + line: effect.line + ) + } + } + + private struct LongLivingEffect: Hashable { + let id = UUID() + let file: StaticString + let line: UInt + + static func == (lhs: Self, rhs: Self) -> Bool { + lhs.id == rhs.id + } + + func hash(into hasher: inout Hasher) { + self.id.hash(into: &hasher) + } + } + + private struct TestAction { + let origin: Origin + let file: StaticString + let line: UInt + + enum Origin { + case send(LocalAction) + case receive(Action) + } + } + } + + extension TestStore where State == LocalState, Action == LocalAction { + /// Initializes a test store from an initial state, a reducer, and an initial environment. + /// + /// - Parameters: + /// - initialState: The state to start the test from. + /// - reducer: A reducer. + /// - environment: The environment to start the test from. + public convenience init( + initialState: State, + reducer: Reducer, + environment: Environment, + file: StaticString = #file, + line: UInt = #line + ) { + self.init( + environment: environment, + file: file, + fromLocalAction: { $0 }, + initialState: initialState, + line: line, + reducer: reducer, + toLocalState: { $0 } + ) + } + } + + extension TestStore where LocalState: Equatable { + public func send( + _ action: LocalAction, + file: StaticString = #file, + line: UInt = #line, + _ update: @escaping (inout LocalState) throws -> Void = { _ in } + ) { + if !self.receivedActions.isEmpty { + var actions = "" + customDump(self.receivedActions.map(\.action), to: &actions) + XCTFail( + """ + Must handle \(self.receivedActions.count) received \ + action\(self.receivedActions.count == 1 ? "" : "s") before sending an action: … + + Unhandled actions: \(actions) + """, + file: file, line: line + ) + } + var expectedState = self.toLocalState(self.snapshotState) + self.store.send(.init(origin: .send(action), file: file, line: line)) + do { + try update(&expectedState) + } catch { + XCTFail("Threw error: \(error)", file: file, line: line) + } + self.expectedStateShouldMatch( + expected: expectedState, + actual: self.toLocalState(self.snapshotState), + file: file, + line: line + ) + if "\(self.file)" == "\(file)" { + self.line = line + } + } + + private func expectedStateShouldMatch( + expected: LocalState, + actual: LocalState, + file: StaticString, + line: UInt + ) { + if expected != actual { + let difference = + diff(expected, actual, format: .proportional) + .map { "\($0.indent(by: 4))\n\n(Expected: −, Actual: +)" } + ?? """ + Expected: + \(String(describing: expected).indent(by: 2)) + + Actual: + \(String(describing: actual).indent(by: 2)) + """ + + XCTFail( + """ + State change does not match expectation: … + + \(difference) + """, + file: file, + line: line + ) + } + } + } + + extension TestStore where LocalState: Equatable, Action: Equatable { + public func receive( + _ expectedAction: Action, + file: StaticString = #file, + line: UInt = #line, + _ update: @escaping (inout LocalState) throws -> Void = { _ in } + ) { + guard !self.receivedActions.isEmpty else { + XCTFail( + """ + Expected to receive an action, but received none. + """, + file: file, line: line + ) + return + } + let (receivedAction, state) = self.receivedActions.removeFirst() + if expectedAction != receivedAction { + let difference = + diff(expectedAction, receivedAction, format: .proportional) + .map { "\($0.indent(by: 4))\n\n(Expected: −, Received: +)" } + ?? """ + Expected: + \(String(describing: expectedAction).indent(by: 2)) + + Received: + \(String(describing: receivedAction).indent(by: 2)) + """ + + XCTFail( + """ + Received unexpected action: … + + \(difference) + """, + file: file, line: line + ) + } + var expectedState = self.toLocalState(self.snapshotState) + do { + try update(&expectedState) + } catch { + XCTFail("Threw error: \(error)", file: file, line: line) + } + expectedStateShouldMatch( + expected: expectedState, + actual: self.toLocalState(state), + file: file, + line: line + ) + snapshotState = state + if "\(self.file)" == "\(file)" { + self.line = line + } + } + } + + extension TestStore { + /// Scopes a store to assert against more local state and actions. + /// + /// Useful for testing view store-specific state and actions. + /// + /// - Parameters: + /// - toLocalState: A function that transforms the reducer's state into more local state. This + /// state will be asserted against as it is mutated by the reducer. Useful for testing view + /// store state transformations. + /// - fromLocalAction: A function that wraps a more local action in the reducer's action. + /// Local actions can be "sent" to the store, while any reducer action may be received. + /// Useful for testing view store action transformations. + public func scope( + state toLocalState: @escaping (LocalState) -> S, + action fromLocalAction: @escaping (A) -> LocalAction + ) -> TestStore { + .init( + environment: self.environment, + file: self.file, + fromLocalAction: { self.fromLocalAction(fromLocalAction($0)) }, + initialState: self.store.state, + line: self.line, + reducer: self.reducer, + toLocalState: { toLocalState(self.toLocalState($0)) } + ) + } + + /// Scopes a store to assert against more local state. + /// + /// Useful for testing view store-specific state. + /// + /// - Parameter toLocalState: A function that transforms the reducer's state into more local + /// state. This state will be asserted against as it is mutated by the reducer. Useful for + /// testing view store state transformations. + public func scope( + state toLocalState: @escaping (LocalState) -> S + ) -> TestStore { + self.scope(state: toLocalState, action: { $0 }) + } + } +#endif diff --git a/Sources/ComposableArchitecture/UIKit/AlertStateUIKit.swift b/Sources/ComposableArchitecture/UIKit/AlertStateUIKit.swift new file mode 100644 index 0000000..d27468a --- /dev/null +++ b/Sources/ComposableArchitecture/UIKit/AlertStateUIKit.swift @@ -0,0 +1,108 @@ +#if canImport(UIKit) && !os(watchOS) + import UIKit + + @available(iOS 13, *) + @available(macCatalyst 13, *) + @available(macOS, unavailable) + @available(tvOS 13, *) + @available(watchOS, unavailable) + extension UIAlertController { + /// Creates a `UIAlertController` from `AlertState`. + /// + /// ```swift + /// class ParentViewController: UIViewController { + /// let store: Store + /// let viewStore: ViewStore + /// private var cancellables: Set = [] + /// private weak var alertController: UIAlertController? + /// ... + /// func viewDidLoad() { + /// ... + /// viewStore.publisher + /// .settingsAlert + /// .sink { [weak self] alert in + /// guard let self = self else { return } + /// if let alert = alert { + /// let alertController = UIAlertController(state: alert, send: { + /// self.viewStore.send(.settings($0)) + /// }) + /// self.present(alertController, animated: true, completion: nil) + /// self.alertController = alertController + /// } else { + /// self.alertController?.dismiss(animated: true, completion: nil) + /// self.alertController = nil + /// } + /// } + /// .store(in: &cancellables) + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - state: The state of an alert that can be shown to the user. + /// - send: A function that wraps an alert action in the view store's action type. + public convenience init( + state: AlertState, + send: @escaping (Action) -> Void + ) { + self.init( + title: String(state: state.title), + message: state.message.map { String(state: $0) }, + preferredStyle: .alert + ) + for button in state.buttons { + self.addAction(button.toUIAlertAction(send: send)) + } + } + + /// Creates a `UIAlertController` from `ConfirmationDialogState`. + /// + /// - Parameters: + /// - state: The state of dialog that can be shown to the user. + /// - send: A function that wraps a dialog action in the view store's action type. + public convenience init( + state: ConfirmationDialogState, send: @escaping (Action) -> Void + ) { + self.init( + title: String(state: state.title), + message: state.message.map { String(state: $0) }, + preferredStyle: .actionSheet + ) + state.buttons.forEach { button in + self.addAction(button.toUIAlertAction(send: send)) + } + } + } + + @available(iOS 13, *) + @available(macCatalyst 13, *) + @available(macOS, unavailable) + @available(tvOS 13, *) + @available(watchOS, unavailable) + extension AlertState.ButtonRole { + var toUIKit: UIAlertAction.Style { + switch self { + case .cancel: + return .cancel + case .destructive: + return .destructive + } + } + } + + @available(iOS 13, *) + @available(macCatalyst 13, *) + @available(macOS, unavailable) + @available(tvOS 13, *) + @available(watchOS, unavailable) + extension AlertState.Button { + func toUIAlertAction(send: @escaping (Action) -> Void) -> UIAlertAction { + let action = self.toSwiftUIAction(send: send) + return UIAlertAction( + title: String(state: self.label), + style: self.role?.toUIKit ?? .default, + handler: { _ in action() } + ) + } + } +#endif diff --git a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift index 4984029..232fa52 100644 --- a/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift +++ b/Sources/ComposableArchitecture/UIKit/IfLetUIKit.swift @@ -1,12 +1,55 @@ import ReactiveSwift extension Store { - @discardableResult + /// Calls one of two closures depending on whether a store's optional state is `nil` or not, and + /// whenever this condition changes for as long as the cancellable lives. + /// + /// If the store's state is non-`nil`, it will safely unwrap the value and bundle it into a new + /// store of non-optional state that is passed to the first closure. If the store's state is + /// `nil`, the second closure is called instead. + /// + /// This method is useful for handling navigation in UIKit. The state for a screen the user wants + /// to navigate to can be held as an optional value in the parent, and when that value goes from + /// `nil` to non-`nil`, or non-`nil` to `nil`, you can update the navigation stack accordingly: + /// + /// ```swift + /// class ParentViewController: UIViewController { + /// let store: Store + /// var cancellables: Set = [] + /// ... + /// func viewDidLoad() { + /// ... + /// self.store + /// .scope(state: \.optionalChild, action: ParentAction.child) + /// .ifLet( + /// then: { [weak self] childStore in + /// self?.navigationController?.pushViewController( + /// ChildViewController(store: childStore), + /// animated: true + /// ) + /// }, + /// else: { [weak self] in + /// guard let self = self else { return } + /// self.navigationController?.popToViewController(self, animated: true) + /// } + /// ) + /// .store(in: &self.cancellables) + /// } + /// } + /// ``` + /// + /// - Parameters: + /// - unwrap: A function that is called with a store of non-optional state when the store's + /// state is non-`nil`, or whenever it goes from `nil` to non-`nil`. + /// - else: A function that is called when the store's optional state is `nil`, or whenever it + /// goes from non-`nil` to `nil`. + /// - Returns: A cancellable that maintains a subscription to updates whenever the store's state + /// goes from `nil` to non-`nil` and vice versa, so that the caller can react to these changes. public func ifLet( then unwrap: @escaping (Store) -> Void, else: @escaping () -> Void = {} ) -> Disposable where State == Wrapped? { - return self.state + return self.$state .skipRepeats({($0 != nil) == ($1 != nil)}) .producer.startWithValues { state in if var state = state { diff --git a/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift b/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift index d768bdd..10632ff 100644 --- a/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift +++ b/Sources/ComposableArchitecture/UIKit/UIKitAnimationScheduler.swift @@ -1,177 +1,203 @@ #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 + import UIKit + import ReactiveSwift + + extension Scheduler { + /// Wraps scheduled actions in `UIView.animate`. + /// + /// - Parameter duration: The `duration` parameter passed to `UIView.animate`. + /// - Parameter delay: The `delay` parameter passed to `UIView.animate`. + /// - Parameter animationOptions: The `options` parameter passed to `UIView.animate` + /// - Returns: A scheduler that wraps scheduled actions in `UIView.animate`. + 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) + } + + /// Wraps scheduled actions in `UIView.animate`. + /// + /// - Parameter duration: The `duration` parameter passed to `UIView.animate`. + /// - Parameter delay: The `delay` parameter passed to `UIView.animate`. + /// - Parameter dampingRatio: The `dampingRatio` parameter passed to `UIView.animate` + /// - Parameter velocity: The `velocity` parameter passed to `UIView.animate` + /// - Parameter animationOptions: The `options` parameter passed to `UIView.animate` + /// - Returns: A scheduler that wraps scheduled actions in `UIView.animate`. + 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 + + extension DateScheduler { + /// Wraps scheduled actions in `UIView.animate`. + /// + /// - Parameter duration: The `duration` parameter passed to `UIView.animate`. + /// - Parameter delay: The `delay` parameter passed to `UIView.animate`. + /// - Parameter animationOptions: The `options` parameter passed to `UIView.animate` + /// - Returns: A scheduler that wraps scheduled actions in `UIView.animate`. + 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) + } + + /// Wraps scheduled actions in `UIView.animate`. + /// + /// - Parameter duration: The `duration` parameter passed to `UIView.animate`. + /// - Parameter delay: The `delay` parameter passed to `UIView.animate`. + /// - Parameter dampingRatio: The `dampingRatio` parameter passed to `UIView.animate` + /// - Parameter velocity: The `velocity` parameter passed to `UIView.animate` + /// - Parameter animationOptions: The `options` parameter passed to `UIView.animate` + /// - Returns: A scheduler that wraps scheduled actions in `UIView.animate`. + 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 + + 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 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 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 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)) + } } - - 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/ViewStore.swift b/Sources/ComposableArchitecture/ViewStore.swift index de07f1c..b28db21 100644 --- a/Sources/ComposableArchitecture/ViewStore.swift +++ b/Sources/ComposableArchitecture/ViewStore.swift @@ -1,52 +1,133 @@ -#if canImport(Combine) import Combine -#endif -#if canImport(SwiftUI) import SwiftUI -#endif import ReactiveSwift + /// A ``ViewStore`` is an object that can observe state changes and send actions. They are most + /// commonly used in views, such as SwiftUI views, UIView or UIViewController, but they can be + /// used anywhere it makes sense to observe state and send actions. + /// + /// In SwiftUI applications, a ``ViewStore`` is accessed most commonly using the ``WithViewStore`` + /// view. It can be initialized with a store and a closure that is handed a view store and must + /// return a view to be rendered: + /// + /// ```swift + /// var body: some View { + /// WithViewStore(self.store) { viewStore in + /// VStack { + /// Text("Current count: \(viewStore.count)") + /// Button("Increment") { viewStore.send(.incrementButtonTapped) } + /// } + /// } + /// } + /// ``` + /// + /// In UIKit applications a ``ViewStore`` can be created from a ``Store`` and then subscribed to for + /// state updates: + /// + /// ```swift + /// let store: Store + /// let viewStore: ViewStore + /// + /// init(store: Store) { + /// self.store = store + /// self.viewStore = ViewStore(store) + /// } + /// + /// func viewDidLoad() { + /// super.viewDidLoad() + /// + /// self.viewStore.publisher.count + /// .sink { [weak self] in self?.countLabel.text = $0 } + /// .store(in: &self.cancellables) + /// } + /// + /// @objc func incrementButtonTapped() { + /// self.viewStore.send(.incrementButtonTapped) + /// } + /// ``` + /// + /// ### Thread safety + /// + /// The ``ViewStore`` class is not thread-safe, and all interactions with it (and the store it was + /// derived from) must happen on the same thread. Further, for SwiftUI applications, all + /// interactions must happen on the _main_ thread. See the documentation of the ``Store`` class for + /// more information as to why this decision was made. @dynamicMemberLookup -public final class ViewStore { -#if canImport(Combine) - @available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *) +public final class ViewStore: ObservableObject { + // N.B. `ViewStore` does not use a `@Published` property, so `objectWillChange` + // won't be synthesized automatically. To work around issues on iOS 13 we explicitly declare it. public private(set) lazy var objectWillChange = ObservableObjectPublisher() -#endif private let _send: (Action) -> Void - fileprivate var _state: MutableProperty + @MutableProperty fileprivate var _state: State private var viewDisposable: Disposable? private let (lifetime, token) = Lifetime.make() + /// Initializes a view store from a store. + /// + /// - Parameters: + /// - store: A store. + /// - isDuplicate: A function to determine when two `State` values are equal. When values are + /// equal, repeat view computations are removed. 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 + self._state = store.state + 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 + self.objectWillChange.send() + self._state = $0 } } + /// A publisher that emits when state changes. + /// + /// This publisher supports dynamic member lookup so that you can pluck out a specific field in + /// the state: + /// + /// ```swift + /// viewStore.publisher.alert + /// .sink { ... } + /// ``` + /// + /// When the emission happens the ``ViewStore``'s state has been updated, and so the following + /// precondition will pass: + /// + /// ```swift + /// viewStore.publisher + /// .sink { precondition($0 == viewStore.state) } + /// ``` + /// + /// This means you can either use the value passed to the closure or you can reach into + /// `viewStore.state` directly. + /// + /// - Note: Due to a bug in Combine (or feature?), the order you `.sink` on a publisher has no + /// bearing on the order the `.sink` closures are called. This means the work performed inside + /// `viewStore.publisher.sink` closures should be completely independent of each other. + /// Later closures cannot assume that earlier ones have already run. public var publisher: StorePublisher { StorePublisher(viewStore: self) } + /// The current state. public var state: State { - self._state.value + self._state + } + + /// Returns the resulting value of a given key path. + public subscript(dynamicMember keyPath: KeyPath) -> LocalState { + self.state[keyPath: keyPath] } + /// makeBindingTarget + /// binding view to viewstore + /// ```swiftt + /// disposables += viewStore.action <~ buttonLogout.reactive.controlEvents(.touchUpInside).map {_ in ViewAction.logout} + /// ``` public func makeBindingTarget(_ action: @escaping (ViewStore, U) -> Void) -> BindingTarget { return BindingTarget(on: UIScheduler(), lifetime: lifetime) { [weak self] value in if let self = self { @@ -54,59 +135,164 @@ public final class ViewStore { } } } - + + /// binding action + /// binding view to viewstore + /// ```swiftt + /// disposables += viewStore.action <~ buttonLogout.reactive.controlEvents(.touchUpInside).map {_ in ViewAction.logout} + /// ``` public var action: BindingTarget { makeBindingTarget { $0.send($1) } } - public subscript(dynamicMember keyPath: KeyPath) -> LocalState { - self.state[keyPath: keyPath] - } - + /// Sends an action to the store. + /// + /// ``ViewStore`` is not thread safe and you should only send actions to it from the main thread. + /// If you are wanting to send actions on background threads due to the fact that the reducer + /// is performing computationally expensive work, then a better way to handle this is to wrap + /// that work in an ``Effect`` that is performed on a background thread so that the result can + /// be fed back into the store. + /// + /// - Parameter action: An action. 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)) } - } + /// Derives a binding from the store that prevents direct writes to state and instead sends + /// actions to the store. + /// + /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s + /// since the ``Store`` does not allow directly writing its state; it only allows reading state + /// and sending actions. + /// + /// For example, a text field binding can be created like this: + /// + /// ```swift + /// struct State { var name = "" } + /// enum Action { case nameChanged(String) } + /// + /// TextField( + /// "Enter name", + /// text: viewStore.binding( + /// get: { $0.name }, + /// send: { Action.nameChanged($0) } + /// ) + /// ) + /// ``` + /// + /// - Parameters: + /// - get: A function to get the state for the binding from the view + /// store's full state. + /// - localStateToViewAction: A function that transforms the binding's value + /// into an action that can be sent to the store. + /// - Returns: A binding. + 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)] + } + + /// Derives a binding from the store that prevents direct writes to state and instead sends + /// actions to the store. + /// + /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s + /// since the ``Store`` does not allow directly writing its state; it only allows reading state + /// and sending actions. + /// + /// For example, an alert binding can be dealt with like this: + /// + /// ```swift + /// struct State { var alert: String? } + /// enum Action { case alertDismissed } + /// + /// .alert( + /// item: self.store.binding( + /// get: { $0.alert }, + /// send: .alertDismissed + /// ) + /// ) { alert in Alert(title: Text(alert.message)) } + /// ``` + /// + /// - Parameters: + /// - get: A function to get the state for the binding from the view store's full state. + /// - action: The action to send when the binding is written to. + /// - Returns: A binding. + public func binding( + get: @escaping (State) -> LocalState, + send action: Action + ) -> Binding { + self.binding(get: get, send: { _ in action }) + } + + /// Derives a binding from the store that prevents direct writes to state and instead sends + /// actions to the store. + /// + /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s + /// since the ``Store`` does not allow directly writing its state; it only allows reading state + /// and sending actions. + /// + /// For example, a text field binding can be created like this: + /// + /// ```swift + /// typealias State = String + /// enum Action { case nameChanged(String) } + /// + /// TextField( + /// "Enter name", + /// text: viewStore.binding( + /// send: { Action.nameChanged($0) } + /// ) + /// ) + /// ``` + /// + /// - Parameters: + /// - localStateToViewAction: A function that transforms the binding's value + /// into an action that can be sent to the store. + /// - Returns: A binding. + public func binding( + send localStateToViewAction: @escaping (State) -> Action + ) -> Binding { + self.binding(get: { $0 }, send: localStateToViewAction) + } + + /// Derives a binding from the store that prevents direct writes to state and instead sends + /// actions to the store. + /// + /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s + /// since the ``Store`` does not allow directly writing its state; it only allows reading state + /// and sending actions. + /// + /// For example, an alert binding can be dealt with like this: + /// + /// ```swift + /// typealias State = String + /// enum Action { case alertDismissed } + /// + /// .alert( + /// item: viewStore.binding( + /// send: .alertDismissed + /// ) + /// ) { title in Alert(title: Text(title)) } + /// ``` + /// + /// - Parameters: + /// - action: The action to send when the binding is written to. + /// - Returns: A binding. + public func binding(send action: Action) -> Binding { + self.binding(send: { _ in action }) + } + + 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() @@ -125,23 +311,20 @@ extension ViewStore where State == Void { } } -#if canImport(Combine) -extension ViewStore: ObservableObject { -} -#endif - + /// A publisher of store state. @dynamicMemberLookup public struct StorePublisher: SignalProducerConvertible { public let upstream: Effect public let viewStore: Any + /// The `SignalProducer` representation of `self`. public var producer: Effect { upstream } fileprivate init(viewStore: ViewStore) { self.viewStore = viewStore - self.upstream = viewStore._state.producer + self.upstream = Property(initial: viewStore.state, then: viewStore.$_state).producer } private init(upstream: Effect,viewStore: Any) { @@ -149,6 +332,7 @@ public struct StorePublisher: SignalProducerConvertible { self.viewStore = viewStore } + /// Returns the resulting publisher of a given key path. public subscript(dynamicMember keyPath: KeyPath) -> StorePublisher where LocalState: Equatable { .init(upstream: self.upstream.map(keyPath).skipRepeats(), viewStore: self.viewStore) } @@ -159,3 +343,220 @@ private struct HashableWrapper: Hashable { static func == (lhs: Self, rhs: Self) -> Bool { false } func hash(into hasher: inout Hasher) {} } + +#if canImport(_Concurrency) && compiler(>=5.5.2) +extension ViewStore { + /// Sends an action into the store and then suspends while a piece of state is `true`. + /// + /// This method can be used to interact with async/await code, allowing you to suspend while + /// work is being performed in an effect. One common example of this is using SwiftUI's + /// `.refreshable` method, which shows a loading indicator on the screen while work is being + /// performed. + /// + /// For example, suppose we wanted to load some data from the network when a pull-to-refresh + /// gesture is performed on a list. The domain and logic for this feature can be modeled like + /// so: + /// + /// ```swift + /// struct State: Equatable { + /// var isLoading = false + /// var response: String? + /// } + /// + /// enum Action { + /// case pulledToRefresh + /// case receivedResponse(String?) + /// } + /// + /// struct Environment { + /// var fetch: () -> Effect + /// } + /// + /// let reducer = Reducer { state, action, environment in + /// switch action { + /// case .pulledToRefresh: + /// state.isLoading = true + /// return environment.fetch() + /// .map(Action.receivedResponse) + /// + /// case let .receivedResponse(response): + /// state.isLoading = false + /// state.response = response + /// return .none + /// } + /// } + /// ``` + /// + /// Note that we keep track of an `isLoading` boolean in our state so that we know exactly + /// when the network response is being performed. + /// + /// The view can show the fact in a `List`, if it's present, and we can use the `.refreshable` + /// view modifier to enhance the list with pull-to-refresh capabilities: + /// + /// ```swift + /// struct MyView: View { + /// let store: Store + /// + /// var body: some View { + /// WithViewStore(self.store) { viewStore in + /// List { + /// if let response = viewStore.response { + /// Text(response) + /// } + /// } + /// .refreshable { + /// await viewStore.send(.pulledToRefresh, while: \.isLoading) + /// } + /// } + /// } + /// } + /// ``` + /// + /// Here we've used the ``send(_:while:)`` method to suspend while the `isLoading` state is + /// `true`. Once that piece of state flips back to `false` the method will resume, signaling + /// to `.refreshable` that the work has finished which will cause the loading indicator to + /// disappear. + /// + /// **Note:** ``ViewStore`` is not thread safe and you should only send actions to it from the + /// main thread. If you are wanting to send actions on background threads due to the fact that + /// the reducer is performing computationally expensive work, then a better way to handle this + /// is to wrap that work in an ``Effect`` that is performed on a background thread so that the + /// result can be fed back into the store. + /// + /// - Parameters: + /// - action: An action. + /// - predicate: A predicate on `State` that determines for how long this method should + /// suspend. + public func send( + _ action: Action, + while predicate: @escaping (State) -> Bool + ) async { + self.send(action) + await self.suspend(while: predicate) + } + + /// Sends an action into the store and then suspends while a piece of state is `true`. + /// + /// See the documentation of ``send(_:while:)`` for more information. + /// + /// - Parameters: + /// - action: An action. + /// - animation: The animation to perform when the action is sent. + /// - predicate: A predicate on `State` that determines for how long this method should + /// suspend. + public func send( + _ action: Action, + animation: Animation?, + while predicate: @escaping (State) -> Bool + ) async { + withAnimation(animation) { self.send(action) } + await self.suspend(while: predicate) + } + + /// Suspends while a predicate on state is `true`. + /// + /// - Parameter predicate: A predicate on `State` that determines for how long this method + /// should suspend. + public func suspend(while predicate: @escaping (State) -> Bool) async { + if #available(iOS 15, macOS 12, tvOS 15, watchOS 8, *) { + _ = try? await self.publisher.producer + .values + .first(where: { !predicate($0) }) + } else { + 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 + +extension Store { + /// swith a store to ViewStore + /// - Returns: ViewStore + public func asViewStore(removeDuplicates isDuplicate: @escaping (State, State) -> Bool) -> ViewStore { + ViewStore(self, removeDuplicates: isDuplicate) + } + +} + +extension Store where State: Equatable { + /// swith a store to ViewStore + /// - Returns: ViewStore + public func asViewStore() -> ViewStore { + ViewStore(self, removeDuplicates: ==) + } +} + +// ReactiveSwift have not yet support async, It would be delete in the future +#if swift(>=5.5.2) && canImport(_Concurrency) && !os(Linux) + import Foundation + + @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, *) + extension SignalProducer { + public var values: AsyncThrowingStream { + AsyncThrowingStream { continuation in + let disposable = start { event in + switch event { + case .value(let value): + continuation.yield(value) + case .completed, .interrupted: + continuation.finish() + case .failed(let error): + continuation.finish(throwing: error) + } + } + continuation.onTermination = { @Sendable _ in + disposable.dispose() + } + } + } + } + +// @available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, macCatalyst 13, *) +// extension SignalProducer where Error == Never { +// @MainActor public var values: AsyncStream { +// AsyncStream { continuation in +// let disposable = start { event in +// switch event { +// case .value(let value): +// continuation.yield(value) +// case .completed, .interrupted: +// continuation.finish() +// case .failed: +// break +// } +// } +// continuation.onTermination = { @Sendable _ in +// disposable.dispose() +// } +// } +// } +// } + #endif diff --git a/Tests/ComposableArchitectureTests/BindingTests.swift b/Tests/ComposableArchitectureTests/BindingTests.swift new file mode 100644 index 0000000..43cda33 --- /dev/null +++ b/Tests/ComposableArchitectureTests/BindingTests.swift @@ -0,0 +1,39 @@ +#if compiler(>=5.4) + import ComposableArchitecture + import XCTest + + final class BindingTests: XCTestCase { + func testNestedBindableState() { + struct State: Equatable { + @BindableState var nested = Nested() + + struct Nested: Equatable { + var field = "" + } + } + + enum Action: BindableAction, Equatable { + case binding(BindingAction) + } + + let reducer = Reducer { state, action, _ in + switch action { + case .binding(\.$nested.field): + state.nested.field += "!" + return .none + default: + return .none + } + } + .binding() + + let store = Store(initialState: .init(), reducer: reducer, environment: ()) + + let viewStore = ViewStore(store) + + viewStore.binding(\.$nested.field).wrappedValue = "Hello" + + XCTAssertNoDifference(viewStore.state, .init(nested: .init(field: "Hello!"))) + } + } +#endif diff --git a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift index fecc4ab..a1dce53 100644 --- a/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift +++ b/Tests/ComposableArchitectureTests/ComposableArchitectureTests.swift @@ -1 +1,162 @@ -import Foundation +import ComposableArchitecture +import XCTest + +final class ComposableArchitectureTests: XCTestCase { + + func testScheduling() { + enum CounterAction: Equatable { + case incrAndSquareLater + case incrNow + case squareNow + } + + let counterReducer = Reducer { + state, action, scheduler in + switch action { + case .incrAndSquareLater: + return .merge( + Effect(value: .incrNow) + .delay(2, on: scheduler) + .eraseToEffect(), + Effect(value: .squareNow) + .delay(1, on: scheduler) + .eraseToEffect(), + Effect(value: .squareNow) + .delay(2, on: scheduler) + .eraseToEffect() + ) + case .incrNow: + state += 1 + return .none + case .squareNow: + state *= state + return .none + } + } + + let scheduler = TestScheduler() + + let store = TestStore( + initialState: 2, + reducer: counterReducer, + environment: scheduler + ) + + store.send(.incrAndSquareLater) + scheduler.advance(by: 1) + store.receive(.squareNow) { $0 = 4 } + scheduler.advance(by: 1) + store.receive(.incrNow) { $0 = 5 } + store.receive(.squareNow) { $0 = 25 } + + store.send(.incrAndSquareLater) + scheduler.advance(by: 2) + store.receive(.squareNow) { $0 = 625 } + store.receive(.incrNow) { $0 = 626 } + store.receive(.squareNow) { $0 = 391876 } + } + + func testSimultaneousWorkOrdering() { + let testScheduler = TestScheduler() + var values: [Int] = [] + testScheduler.schedule(after: .seconds(0), interval: .seconds(1)) { values.append(1) } + testScheduler.schedule(after: .seconds(0), interval: .seconds(2)) { values.append(42) } + + XCTAssertNoDifference(values, []) + testScheduler.advance() + XCTAssertNoDifference(values, [1, 42]) + testScheduler.advance(by: 2) + XCTAssertNoDifference(values, [1, 42, 1, 42, 1]) + } + + func testLongLivingEffects() { + typealias Environment = ( + startEffect: Effect, + stopEffect: Effect + ) + + enum Action { case end, incr, start } + + let reducer = Reducer { state, action, environment in + switch action { + case .end: + return environment.stopEffect.fireAndForget() + case .incr: + state += 1 + return .none + case .start: + return environment.startEffect.map { Action.incr } + } + } + + let subject = Signal.pipe() + + let store = TestStore( + initialState: 0, + reducer: reducer, + environment: ( + startEffect: subject.output.producer, + stopEffect: .fireAndForget { subject.input.sendCompleted() } + ) + ) + + store.send(.start) + store.send(.incr) { $0 = 1 } + subject.input.send(value: ()) + store.receive(.incr) { $0 = 2 } + store.send(.end) + } + + func testCancellation() { + enum Action: Equatable { + case cancel + case incr + case response(Int) + } + + struct Environment { + let fetch: (Int) -> Effect + let mainQueue: DateScheduler + } + + let reducer = Reducer { state, action, environment in + struct CancelId: Hashable {} + + switch action { + case .cancel: + return .cancel(id: CancelId()) + + case .incr: + state += 1 + return environment.fetch(state) + .observe(on: environment.mainQueue) + .map(Action.response) + .eraseToEffect() + .cancellable(id: CancelId()) + + case let .response(value): + state = value + return .none + } + } + + let scheduler = TestScheduler() + + let store = TestStore( + initialState: 0, + reducer: reducer, + environment: Environment( + fetch: { value in Effect(value: value * value) }, + mainQueue: scheduler + ) + ) + + store.send(.incr) { $0 = 1 } + scheduler.advance() + store.receive(.response(1)) { $0 = 1 } + + store.send(.incr) { $0 = 2 } + store.send(.cancel) + scheduler.run() + } +} diff --git a/Tests/ComposableArchitectureTests/DebugTests.swift b/Tests/ComposableArchitectureTests/DebugTests.swift index fecc4ab..ee780ce 100644 --- a/Tests/ComposableArchitectureTests/DebugTests.swift +++ b/Tests/ComposableArchitectureTests/DebugTests.swift @@ -1 +1,205 @@ -import Foundation +import Combine +import CustomDump +import XCTest + +@testable import ComposableArchitecture + +final class DebugTests: XCTestCase { + func testAlertState() { + var dump = "" + customDump( + AlertState( + title: .init("Alert!"), + message: .init("Something went wrong..."), + primaryButton: .destructive(.init("Destroy"), action: .send(true, animation: .default)), + secondaryButton: .cancel(.init("Cancel"), action: .send(false)) + ), + to: &dump + ) + XCTAssertNoDifference( + dump, + """ + AlertState( + title: "Alert!", + message: "Something went wrong...", + buttons: [ + [0]: AlertState.Button.destructive( + "Destroy", + action: AlertState.ButtonAction.send( + true, + animation: Animation.easeInOut + ) + ), + [1]: AlertState.Button.cancel( + "Cancel", + action: AlertState.ButtonAction.send(false) + ) + ] + ) + """ + ) + + if #available(iOS 13, macOS 12, tvOS 13, watchOS 6, *) { + dump = "" + customDump( + ConfirmationDialogState( + title: .init("Alert!"), + message: .init("Something went wrong..."), + buttons: [ + .destructive(.init("Destroy"), action: .send(true, animation: .default)), + .cancel(.init("Cancel"), action: .send(false)), + ] + ), + to: &dump + ) + XCTAssertNoDifference( + dump, + """ + ConfirmationDialogState( + title: "Alert!", + message: "Something went wrong...", + buttons: [ + [0]: AlertState.Button.destructive( + "Destroy", + action: AlertState.ButtonAction.send( + true, + animation: Animation.easeInOut + ) + ), + [1]: AlertState.Button.cancel( + "Cancel", + action: AlertState.ButtonAction.send(false) + ) + ] + ) + """ + ) + } + } + + func testTextState() { + var dump = "" + customDump(TextState("Hello, world!"), to: &dump) + XCTAssertNoDifference( + dump, + """ + "Hello, world!" + """ + ) + + dump = "" + customDump( + TextState("Hello, ") + + TextState("world").bold().italic() + + TextState("!"), + to: &dump + ) + XCTAssertNoDifference( + dump, + """ + "Hello, _**world**_!" + """ + ) + + dump = "" + customDump( + TextState("Offset by 10.5").baselineOffset(10.5) + + TextState("\n") + TextState("Headline").font(.headline) + + TextState("\n") + TextState("No font").font(nil) + + TextState("\n") + TextState("Light font weight").fontWeight(.light) + + TextState("\n") + TextState("No font weight").fontWeight(nil) + + TextState("\n") + TextState("Red").foregroundColor(.red) + + TextState("\n") + TextState("No color").foregroundColor(nil) + + TextState("\n") + TextState("Italic").italic() + + TextState("\n") + TextState("Kerning of 2.5").kerning(2.5) + + TextState("\n") + TextState("Stricken").strikethrough() + + TextState("\n") + TextState("Stricken green").strikethrough(color: .green) + + TextState("\n") + TextState("Not stricken blue").strikethrough(false, color: .blue) + + TextState("\n") + TextState("Tracking of 5.5").tracking(5.5) + + TextState("\n") + TextState("Underlined").underline() + + TextState("\n") + TextState("Underlined pink").underline(color: .pink) + + TextState("\n") + TextState("Not underlined purple").underline(false, color: .pink), + to: &dump + ) + XCTAssertNoDifference( + dump, + #""" + """ + Offset by 10.5 + Headline + No font + Light font weight + No font weight + Red + No color + _Italic_ + Kerning of 2.5 + ~~Stricken~~ + Stricken green + Not stricken blue + Tracking of 5.5 + Underlined + Underlined pink + Not underlined purple + """ + """# + ) + } + + func testDebugCaseOutput() { + enum Action { + case action1(Bool, label: String) + case action2(Bool, Int, String) + case screenA(ScreenA) + + enum ScreenA { + case row(index: Int, action: RowAction) + + enum RowAction { + case tapped + case textChanged(query: String) + } + } + } + + XCTAssertNoDifference( + debugCaseOutput(Action.action1(true, label: "Blob")), + "Action.action1(_:, label:)" + ) + + XCTAssertNoDifference( + debugCaseOutput(Action.action2(true, 1, "Blob")), + "Action.action2(_:, _:, _:)" + ) + + XCTAssertNoDifference( + debugCaseOutput(Action.screenA(.row(index: 1, action: .tapped))), + "Action.screenA(.row(index:, action: .tapped))" + ) + + XCTAssertNoDifference( + debugCaseOutput(Action.screenA(.row(index: 1, action: .textChanged(query: "Hi")))), + "Action.screenA(.row(index:, action: .textChanged(query:)))" + ) + } + + #if compiler(>=5.4) + func testBindingAction() { + struct State { + @BindableState var width = 0 + } + + var dump = "" + customDump(BindingAction.set(\State.$width, 50), to: &dump) + XCTAssertNoDifference( + dump, + #""" + BindingAction.set( + WritableKeyPath>, + 50 + ) + """# + ) + } + #endif +} diff --git a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift index fecc4ab..99e07db 100644 --- a/Tests/ComposableArchitectureTests/EffectCancellationTests.swift +++ b/Tests/ComposableArchitectureTests/EffectCancellationTests.swift @@ -1 +1,305 @@ -import Foundation +//import Combine +//import XCTest +// +//@testable import ComposableArchitecture +// +//final class EffectCancellationTests: XCTestCase { +// struct CancelToken: Hashable {} +// var cancellables: Set = [] +// +// override func tearDown() { +// super.tearDown() +// self.cancellables.removeAll() +// } +// +// func testCancellation() { +// var values: [Int] = [] +// +// let subject = PassthroughSubject() +// let effect = Effect(subject) +// .cancellable(id: CancelToken()) +// +// effect +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, []) +// subject.send(1) +// XCTAssertNoDifference(values, [1]) +// subject.send(2) +// XCTAssertNoDifference(values, [1, 2]) +// +// Effect.cancel(id: CancelToken()) +// .sink { _ in } +// .store(in: &self.cancellables) +// +// subject.send(3) +// XCTAssertNoDifference(values, [1, 2]) +// } +// +// func testCancelInFlight() { +// var values: [Int] = [] +// +// let subject = PassthroughSubject() +// Effect(subject) +// .cancellable(id: CancelToken(), cancelInFlight: true) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, []) +// subject.send(1) +// XCTAssertNoDifference(values, [1]) +// subject.send(2) +// XCTAssertNoDifference(values, [1, 2]) +// +// Effect(subject) +// .cancellable(id: CancelToken(), cancelInFlight: true) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// +// subject.send(3) +// XCTAssertNoDifference(values, [1, 2, 3]) +// subject.send(4) +// XCTAssertNoDifference(values, [1, 2, 3, 4]) +// } +// +// func testCancellationAfterDelay() { +// var value: Int? +// +// Just(1) +// .delay(for: 0.15, scheduler: DispatchQueue.main) +// .eraseToEffect() +// .cancellable(id: CancelToken()) +// .sink { value = $0 } +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(value, nil) +// +// DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { +// Effect.cancel(id: CancelToken()) +// .sink { _ in } +// .store(in: &self.cancellables) +// } +// +// _ = XCTWaiter.wait(for: [self.expectation(description: "")], timeout: 0.3) +// +// XCTAssertNoDifference(value, nil) +// } +// +// func testCancellationAfterDelay_WithTestScheduler() { +// let scheduler = DispatchQueue.test +// var value: Int? +// +// Just(1) +// .delay(for: 2, scheduler: scheduler) +// .eraseToEffect() +// .cancellable(id: CancelToken()) +// .sink { value = $0 } +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(value, nil) +// +// scheduler.advance(by: 1) +// Effect.cancel(id: CancelToken()) +// .sink { _ in } +// .store(in: &self.cancellables) +// +// scheduler.run() +// +// XCTAssertNoDifference(value, nil) +// } +// +// func testCancellablesCleanUp_OnComplete() { +// Just(1) +// .eraseToEffect() +// .cancellable(id: 1) +// .sink(receiveValue: { _ in }) +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference([:], cancellationCancellables) +// } +// +// func testCancellablesCleanUp_OnCancel() { +// let scheduler = DispatchQueue.test +// Just(1) +// .delay(for: 1, scheduler: scheduler) +// .eraseToEffect() +// .cancellable(id: 1) +// .sink(receiveValue: { _ in }) +// .store(in: &self.cancellables) +// +// Effect.cancel(id: 1) +// .sink(receiveValue: { _ in }) +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference([:], cancellationCancellables) +// } +// +// func testDoubleCancellation() { +// var values: [Int] = [] +// +// let subject = PassthroughSubject() +// let effect = Effect(subject) +// .cancellable(id: CancelToken()) +// .cancellable(id: CancelToken()) +// +// effect +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, []) +// subject.send(1) +// XCTAssertNoDifference(values, [1]) +// +// Effect.cancel(id: CancelToken()) +// .sink { _ in } +// .store(in: &self.cancellables) +// +// subject.send(2) +// XCTAssertNoDifference(values, [1]) +// } +// +// func testCompleteBeforeCancellation() { +// var values: [Int] = [] +// +// let subject = PassthroughSubject() +// let effect = Effect(subject) +// .cancellable(id: CancelToken()) +// +// effect +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// +// subject.send(1) +// XCTAssertNoDifference(values, [1]) +// +// subject.send(completion: .finished) +// XCTAssertNoDifference(values, [1]) +// +// Effect.cancel(id: CancelToken()) +// .sink { _ in } +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, [1]) +// } +// +// func testConcurrentCancels() { +// let queues = [ +// DispatchQueue.main, +// DispatchQueue.global(qos: .background), +// DispatchQueue.global(qos: .default), +// DispatchQueue.global(qos: .unspecified), +// DispatchQueue.global(qos: .userInitiated), +// DispatchQueue.global(qos: .userInteractive), +// DispatchQueue.global(qos: .utility), +// ] +// +// let effect = Effect.merge( +// (1...1_000).map { idx -> Effect in +// let id = idx % 10 +// +// return Effect.merge( +// Just(idx) +// .delay( +// for: .milliseconds(Int.random(in: 1...100)), scheduler: queues.randomElement()! +// ) +// .eraseToEffect() +// .cancellable(id: id), +// +// Just(()) +// .delay( +// for: .milliseconds(Int.random(in: 1...100)), scheduler: queues.randomElement()! +// ) +// .flatMap { Effect.cancel(id: id) } +// .eraseToEffect() +// ) +// } +// ) +// +// let expectation = self.expectation(description: "wait") +// effect +// .sink(receiveCompletion: { _ in expectation.fulfill() }, receiveValue: { _ in }) +// .store(in: &self.cancellables) +// self.wait(for: [expectation], timeout: 999) +// +// XCTAssertTrue(cancellationCancellables.isEmpty) +// } +// +// func testNestedCancels() { +// var effect = Empty(completeImmediately: false) +// .eraseToEffect() +// .cancellable(id: 1) +// +// for _ in 1 ... .random(in: 1...1_000) { +// effect = effect.cancellable(id: 1) +// } +// +// effect +// .sink(receiveValue: { _ in }) +// .store(in: &cancellables) +// +// cancellables.removeAll() +// +// XCTAssertNoDifference([:], cancellationCancellables) +// } +// +// func testSharedId() { +// let scheduler = DispatchQueue.test +// +// let effect1 = Just(1) +// .delay(for: 1, scheduler: scheduler) +// .eraseToEffect() +// .cancellable(id: "id") +// +// let effect2 = Just(2) +// .delay(for: 2, scheduler: scheduler) +// .eraseToEffect() +// .cancellable(id: "id") +// +// var expectedOutput: [Int] = [] +// effect1 +// .sink { expectedOutput.append($0) } +// .store(in: &cancellables) +// effect2 +// .sink { expectedOutput.append($0) } +// .store(in: &cancellables) +// +// XCTAssertNoDifference(expectedOutput, []) +// scheduler.advance(by: 1) +// XCTAssertNoDifference(expectedOutput, [1]) +// scheduler.advance(by: 1) +// XCTAssertNoDifference(expectedOutput, [1, 2]) +// } +// +// func testImmediateCancellation() { +// let scheduler = DispatchQueue.test +// +// var expectedOutput: [Int] = [] +// // Don't hold onto cancellable so that it is deallocated immediately. +// _ = Deferred { Just(1) } +// .delay(for: 1, scheduler: scheduler) +// .eraseToEffect() +// .cancellable(id: "id") +// .sink { expectedOutput.append($0) } +// +// XCTAssertNoDifference(expectedOutput, []) +// scheduler.advance(by: 1) +// XCTAssertNoDifference(expectedOutput, []) +// } +// +// func testNestedMergeCancellation() { +// let effect = Effect.merge( +// (1...2).publisher +// .eraseToEffect() +// .cancellable(id: 1) +// ) +// .cancellable(id: 2) +// +// var output: [Int] = [] +// effect +// .sink { output.append($0) } +// .store(in: &cancellables) +// +// XCTAssertEqual(output, [1, 2]) +// } +//} diff --git a/Tests/ComposableArchitectureTests/EffectDebounceTests.swift b/Tests/ComposableArchitectureTests/EffectDebounceTests.swift index fecc4ab..98b2914 100644 --- a/Tests/ComposableArchitectureTests/EffectDebounceTests.swift +++ b/Tests/ComposableArchitectureTests/EffectDebounceTests.swift @@ -1 +1,86 @@ -import Foundation +//import Combine +//import ComposableArchitecture +//import XCTest +// +//final class EffectDebounceTests: XCTestCase { +// var cancellables: Set = [] +// +// func testDebounce() { +// let scheduler = DispatchQueue.test +// var values: [Int] = [] +// +// func runDebouncedEffect(value: Int) { +// struct CancelToken: Hashable {} +// Just(value) +// .eraseToEffect() +// .debounce(id: CancelToken(), for: 1, scheduler: scheduler) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runDebouncedEffect(value: 1) +// +// // Nothing emits right away. +// XCTAssertNoDifference(values, []) +// +// // Waiting half the time also emits nothing +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, []) +// +// // Run another debounced effect. +// runDebouncedEffect(value: 2) +// +// // Waiting half the time emits nothing because the first debounced effect has been canceled. +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, []) +// +// // Run another debounced effect. +// runDebouncedEffect(value: 3) +// +// // Waiting half the time emits nothing because the second debounced effect has been canceled. +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, []) +// +// // Waiting the rest of the time emits the final effect value. +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, [3]) +// +// // Running out the scheduler +// scheduler.run() +// XCTAssertNoDifference(values, [3]) +// } +// +// func testDebounceIsLazy() { +// let scheduler = DispatchQueue.test +// var values: [Int] = [] +// var effectRuns = 0 +// +// func runDebouncedEffect(value: Int) { +// struct CancelToken: Hashable {} +// +// Deferred { () -> Just in +// effectRuns += 1 +// return Just(value) +// } +// .eraseToEffect() +// .debounce(id: CancelToken(), for: 1, scheduler: scheduler) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runDebouncedEffect(value: 1) +// +// XCTAssertNoDifference(values, []) +// XCTAssertNoDifference(effectRuns, 0) +// +// scheduler.advance(by: 0.5) +// +// XCTAssertNoDifference(values, []) +// XCTAssertNoDifference(effectRuns, 0) +// +// scheduler.advance(by: 0.5) +// +// XCTAssertNoDifference(values, [1]) +// XCTAssertNoDifference(effectRuns, 1) +// } +//} diff --git a/Tests/ComposableArchitectureTests/EffectDeferredTests.swift b/Tests/ComposableArchitectureTests/EffectDeferredTests.swift index fecc4ab..7c6933f 100644 --- a/Tests/ComposableArchitectureTests/EffectDeferredTests.swift +++ b/Tests/ComposableArchitectureTests/EffectDeferredTests.swift @@ -1 +1,83 @@ -import Foundation +//import Combine +//import ComposableArchitecture +//import XCTest +// +//final class EffectDeferredTests: XCTestCase { +// var cancellables: Set = [] +// +// func testDeferred() { +// let scheduler = DispatchQueue.test +// var values: [Int] = [] +// +// func runDeferredEffect(value: Int) { +// Just(value) +// .eraseToEffect() +// .deferred(for: 1, scheduler: scheduler) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runDeferredEffect(value: 1) +// +// // Nothing emits right away. +// XCTAssertNoDifference(values, []) +// +// // Waiting half the time also emits nothing +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, []) +// +// // Run another deferred effect. +// runDeferredEffect(value: 2) +// +// // Waiting half the time emits first deferred effect received. +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, [1]) +// +// // Run another deferred effect. +// runDeferredEffect(value: 3) +// +// // Waiting half the time emits second deferred effect received. +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, [1, 2]) +// +// // Waiting the rest of the time emits the final effect value. +// scheduler.advance(by: 0.5) +// XCTAssertNoDifference(values, [1, 2, 3]) +// +// // Running out the scheduler +// scheduler.run() +// XCTAssertNoDifference(values, [1, 2, 3]) +// } +// +// func testDeferredIsLazy() { +// let scheduler = DispatchQueue.test +// var values: [Int] = [] +// var effectRuns = 0 +// +// func runDeferredEffect(value: Int) { +// Deferred { () -> Just in +// effectRuns += 1 +// return Just(value) +// } +// .eraseToEffect() +// .deferred(for: 1, scheduler: scheduler) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runDeferredEffect(value: 1) +// +// XCTAssertNoDifference(values, []) +// XCTAssertNoDifference(effectRuns, 0) +// +// scheduler.advance(by: 0.5) +// +// XCTAssertNoDifference(values, []) +// XCTAssertNoDifference(effectRuns, 0) +// +// scheduler.advance(by: 0.5) +// +// XCTAssertNoDifference(values, [1]) +// XCTAssertNoDifference(effectRuns, 1) +// } +//} diff --git a/Tests/ComposableArchitectureTests/EffectTests.swift b/Tests/ComposableArchitectureTests/EffectTests.swift index fecc4ab..23fbdfd 100644 --- a/Tests/ComposableArchitectureTests/EffectTests.swift +++ b/Tests/ComposableArchitectureTests/EffectTests.swift @@ -1 +1,287 @@ -import Foundation +//import Combine +//import XCTest +// +//@testable import ComposableArchitecture +// +//final class EffectTests: XCTestCase { +// var cancellables: Set = [] +// let scheduler = DispatchQueue.test +// +// func testCatchToEffect() { +// struct Error: Swift.Error, Equatable {} +// +// Future { $0(.success(42)) } +// .catchToEffect() +// .sink { XCTAssertNoDifference($0, .success(42)) } +// .store(in: &self.cancellables) +// +// Future { $0(.failure(Error())) } +// .catchToEffect() +// .sink { XCTAssertNoDifference($0, .failure(Error())) } +// .store(in: &self.cancellables) +// +// Future { $0(.success(42)) } +// .eraseToEffect() +// .sink { XCTAssertNoDifference($0, 42) } +// .store(in: &self.cancellables) +// +// Future { $0(.success(42)) } +// .catchToEffect { +// switch $0 { +// case let .success(val): +// return val +// case .failure: +// return -1 +// } +// } +// .sink { XCTAssertNoDifference($0, 42) } +// .store(in: &self.cancellables) +// +// Future { $0(.failure(Error())) } +// .catchToEffect { +// switch $0 { +// case let .success(val): +// return val +// case .failure: +// return -1 +// } +// } +// .sink { XCTAssertNoDifference($0, -1) } +// .store(in: &self.cancellables) +// } +// +// func testConcatenate() { +// var values: [Int] = [] +// +// let effect = Effect.concatenate( +// Effect(value: 1).delay(for: 1, scheduler: scheduler).eraseToEffect(), +// Effect(value: 2).delay(for: 2, scheduler: scheduler).eraseToEffect(), +// Effect(value: 3).delay(for: 3, scheduler: scheduler).eraseToEffect() +// ) +// +// effect.sink(receiveValue: { values.append($0) }).store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, []) +// +// self.scheduler.advance(by: 1) +// XCTAssertNoDifference(values, [1]) +// +// self.scheduler.advance(by: 2) +// XCTAssertNoDifference(values, [1, 2]) +// +// self.scheduler.advance(by: 3) +// XCTAssertNoDifference(values, [1, 2, 3]) +// +// self.scheduler.run() +// XCTAssertNoDifference(values, [1, 2, 3]) +// } +// +// func testConcatenateOneEffect() { +// var values: [Int] = [] +// +// let effect = Effect.concatenate( +// Effect(value: 1).delay(for: 1, scheduler: scheduler).eraseToEffect() +// ) +// +// effect.sink(receiveValue: { values.append($0) }).store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, []) +// +// self.scheduler.advance(by: 1) +// XCTAssertNoDifference(values, [1]) +// +// self.scheduler.run() +// XCTAssertNoDifference(values, [1]) +// } +// +// func testMerge() { +// let effect = Effect.merge( +// Effect(value: 1).delay(for: 1, scheduler: scheduler).eraseToEffect(), +// Effect(value: 2).delay(for: 2, scheduler: scheduler).eraseToEffect(), +// Effect(value: 3).delay(for: 3, scheduler: scheduler).eraseToEffect() +// ) +// +// var values: [Int] = [] +// effect.sink(receiveValue: { values.append($0) }).store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, []) +// +// self.scheduler.advance(by: 1) +// XCTAssertNoDifference(values, [1]) +// +// self.scheduler.advance(by: 1) +// XCTAssertNoDifference(values, [1, 2]) +// +// self.scheduler.advance(by: 1) +// XCTAssertNoDifference(values, [1, 2, 3]) +// } +// +// func testEffectSubscriberInitializer() { +// let effect = Effect.run { subscriber in +// subscriber.send(1) +// subscriber.send(2) +// self.scheduler.schedule(after: self.scheduler.now.advanced(by: .seconds(1))) { +// subscriber.send(3) +// } +// self.scheduler.schedule(after: self.scheduler.now.advanced(by: .seconds(2))) { +// subscriber.send(4) +// subscriber.send(completion: .finished) +// } +// +// return AnyCancellable {} +// } +// +// var values: [Int] = [] +// var isComplete = false +// effect +// .sink(receiveCompletion: { _ in isComplete = true }, receiveValue: { values.append($0) }) +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, [1, 2]) +// XCTAssertNoDifference(isComplete, false) +// +// self.scheduler.advance(by: 1) +// +// XCTAssertNoDifference(values, [1, 2, 3]) +// XCTAssertNoDifference(isComplete, false) +// +// self.scheduler.advance(by: 1) +// +// XCTAssertNoDifference(values, [1, 2, 3, 4]) +// XCTAssertNoDifference(isComplete, true) +// } +// +// func testEffectSubscriberInitializer_WithCancellation() { +// struct CancelId: Hashable {} +// +// let effect = Effect.run { subscriber in +// subscriber.send(1) +// self.scheduler.schedule(after: self.scheduler.now.advanced(by: .seconds(1))) { +// subscriber.send(2) +// } +// +// return AnyCancellable {} +// } +// .cancellable(id: CancelId()) +// +// var values: [Int] = [] +// var isComplete = false +// effect +// .sink(receiveCompletion: { _ in isComplete = true }, receiveValue: { values.append($0) }) +// .store(in: &self.cancellables) +// +// XCTAssertNoDifference(values, [1]) +// XCTAssertNoDifference(isComplete, false) +// +// Effect.cancel(id: CancelId()) +// .sink(receiveValue: { _ in }) +// .store(in: &self.cancellables) +// +// self.scheduler.advance(by: 1) +// +// XCTAssertNoDifference(values, [1]) +// XCTAssertNoDifference(isComplete, true) +// } +// +// func testEffectErrorCrash() { +// let expectation = self.expectation(description: "Complete") +// +// // This crashes on iOS 13 if Effect.init(error:) is implemented using the Fail publisher. +// Effect(error: NSError(domain: "", code: 1)) +// .retry(3) +// .catch { _ in Fail(error: NSError(domain: "", code: 1)) } +// .sink( +// receiveCompletion: { _ in expectation.fulfill() }, +// receiveValue: { _ in } +// ) +// .store(in: &self.cancellables) +// +// self.wait(for: [expectation], timeout: 0) +// } +// +// func testDoubleCancelInFlight() { +// var result: Int? +// +// _ = Just(42) +// .eraseToEffect() +// .cancellable(id: "id", cancelInFlight: true) +// .cancellable(id: "id", cancelInFlight: true) +// .sink { result = $0 } +// +// XCTAssertEqual(result, 42) +// } +// +// #if compiler(>=5.4) +// func testFailing() { +// let effect = Effect.failing("failing") +// XCTExpectFailure { +// effect +// .sink(receiveValue: { _ in }) +// .store(in: &self.cancellables) +// } +// } +// #endif +// +// #if canImport(_Concurrency) && compiler(>=5.5.2) +// func testTask() { +// let expectation = self.expectation(description: "Complete") +// var result: Int? +// Effect.task { +// expectation.fulfill() +// return 42 +// } +// .sink(receiveValue: { result = $0 }) +// .store(in: &self.cancellables) +// self.wait(for: [expectation], timeout: 1) +// XCTAssertNoDifference(result, 42) +// } +// +// func testThrowingTask() { +// let expectation = self.expectation(description: "Complete") +// struct MyError: Error {} +// var result: Error? +// Effect.task { +// expectation.fulfill() +// throw MyError() +// } +// .sink( +// receiveCompletion: { +// switch $0 { +// case .finished: +// XCTFail() +// case let .failure(error): +// result = error +// } +// }, +// receiveValue: { _ in XCTFail() } +// ) +// .store(in: &self.cancellables) +// self.wait(for: [expectation], timeout: 1) +// XCTAssertNotNil(result) +// } +// +// func testCancellingTask() { +// @Sendable func work() async throws -> Int { +// var task: Task! +// task = Task { +// try? await Task.sleep(nanoseconds: NSEC_PER_MSEC) +// try Task.checkCancellation() +// return 42 +// } +// task.cancel() +// return try await task.value +// } +// +// let expectation = self.expectation(description: "Complete") +// Effect.task { +// try await work() +// } +// .sink( +// receiveCompletion: { _ in expectation.fulfill() }, +// receiveValue: { _ in XCTFail() } +// ) +// .store(in: &self.cancellables) +// self.wait(for: [expectation], timeout: 1) +// } +// #endif +//} diff --git a/Tests/ComposableArchitectureTests/EffectThrottleTests.swift b/Tests/ComposableArchitectureTests/EffectThrottleTests.swift index fecc4ab..a319adf 100644 --- a/Tests/ComposableArchitectureTests/EffectThrottleTests.swift +++ b/Tests/ComposableArchitectureTests/EffectThrottleTests.swift @@ -1 +1,209 @@ -import Foundation +//import Combine +//import XCTest +// +//@testable import ComposableArchitecture +// +//final class EffectThrottleTests: XCTestCase { +// var cancellables: Set = [] +// let scheduler = DispatchQueue.test +// +// func testThrottleLatest() { +// var values: [Int] = [] +// var effectRuns = 0 +// +// func runThrottledEffect(value: Int) { +// struct CancelToken: Hashable {} +// +// Deferred { () -> Just in +// effectRuns += 1 +// return Just(value) +// } +// .eraseToEffect() +// .throttle(id: CancelToken(), for: 1, scheduler: scheduler.eraseToAnyScheduler(), latest: true) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runThrottledEffect(value: 1) +// +// scheduler.advance() +// +// // A value emits right away. +// XCTAssertNoDifference(values, [1]) +// +// runThrottledEffect(value: 2) +// +// scheduler.advance() +// +// // A second value is throttled. +// XCTAssertNoDifference(values, [1]) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 3) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 4) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 5) +// +// // A third value is throttled. +// XCTAssertNoDifference(values, [1]) +// +// scheduler.advance(by: 0.25) +// +// // The latest value emits. +// XCTAssertNoDifference(values, [1, 5]) +// } +// +// func testThrottleFirst() { +// var values: [Int] = [] +// var effectRuns = 0 +// +// func runThrottledEffect(value: Int) { +// struct CancelToken: Hashable {} +// +// Deferred { () -> Just in +// effectRuns += 1 +// return Just(value) +// } +// .eraseToEffect() +// .throttle( +// id: CancelToken(), for: 1, scheduler: scheduler.eraseToAnyScheduler(), latest: false +// ) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runThrottledEffect(value: 1) +// +// scheduler.advance() +// +// // A value emits right away. +// XCTAssertNoDifference(values, [1]) +// +// runThrottledEffect(value: 2) +// +// scheduler.advance() +// +// // A second value is throttled. +// XCTAssertNoDifference(values, [1]) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 3) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 4) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 5) +// +// scheduler.advance(by: 0.25) +// +// // The second (throttled) value emits. +// XCTAssertNoDifference(values, [1, 2]) +// +// scheduler.advance(by: 0.25) +// +// runThrottledEffect(value: 6) +// +// scheduler.advance(by: 0.50) +// +// // A third value is throttled. +// XCTAssertNoDifference(values, [1, 2]) +// +// runThrottledEffect(value: 7) +// +// scheduler.advance(by: 0.25) +// +// // The third (throttled) value emits. +// XCTAssertNoDifference(values, [1, 2, 6]) +// } +// +// func testThrottleAfterInterval() { +// var values: [Int] = [] +// var effectRuns = 0 +// +// func runThrottledEffect(value: Int) { +// struct CancelToken: Hashable {} +// +// Deferred { () -> Just in +// effectRuns += 1 +// return Just(value) +// } +// .eraseToEffect() +// .throttle(id: CancelToken(), for: 1, scheduler: scheduler.eraseToAnyScheduler(), latest: true) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runThrottledEffect(value: 1) +// +// scheduler.advance() +// +// // A value emits right away. +// XCTAssertNoDifference(values, [1]) +// +// scheduler.advance(by: 2) +// +// runThrottledEffect(value: 2) +// +// scheduler.advance() +// +// // A second value is emitted right away. +// XCTAssertNoDifference(values, [1, 2]) +// +// scheduler.advance(by: 2) +// +// runThrottledEffect(value: 3) +// +// scheduler.advance() +// +// // A third value is emitted right away. +// XCTAssertNoDifference(values, [1, 2, 3]) +// } +// +// func testThrottleEmitsFirstValueOnce() { +// var values: [Int] = [] +// var effectRuns = 0 +// +// func runThrottledEffect(value: Int) { +// struct CancelToken: Hashable {} +// +// Deferred { () -> Just in +// effectRuns += 1 +// return Just(value) +// } +// .eraseToEffect() +// .throttle( +// id: CancelToken(), for: 1, scheduler: scheduler.eraseToAnyScheduler(), latest: false +// ) +// .sink { values.append($0) } +// .store(in: &self.cancellables) +// } +// +// runThrottledEffect(value: 1) +// +// scheduler.advance() +// +// // A value emits right away. +// XCTAssertNoDifference(values, [1]) +// +// scheduler.advance(by: 0.5) +// +// runThrottledEffect(value: 2) +// +// scheduler.advance(by: 0.5) +// +// runThrottledEffect(value: 3) +// +// // A second value is emitted right away. +// XCTAssertNoDifference(values, [1, 2]) +// } +//} diff --git a/Tests/ComposableArchitectureTests/MemoryManagementTests.swift b/Tests/ComposableArchitectureTests/MemoryManagementTests.swift index fecc4ab..7a4c04a 100644 --- a/Tests/ComposableArchitectureTests/MemoryManagementTests.swift +++ b/Tests/ComposableArchitectureTests/MemoryManagementTests.swift @@ -1 +1,37 @@ -import Foundation +import ComposableArchitecture +import XCTest + +final class MemoryManagementTests: XCTestCase { + + func testOwnership_ScopeHoldsOntoParent() { + let counterReducer = Reducer { state, _, _ in + state += 1 + return .none + } + let store = Store(initialState: 0, reducer: counterReducer, environment: ()) + .scope(state: { "\($0)" }) + .scope(state: { Int($0)! }) + let viewStore = ViewStore(store) + + var count = 0 + viewStore.publisher.producer.startWithValues { count = $0 } + XCTAssertNoDifference(count, 0) + viewStore.send(()) + XCTAssertNoDifference(count, 1) + } + + func testOwnership_ViewStoreHoldsOntoStore() { + let counterReducer = Reducer { state, _, _ in + state += 1 + return .none + } + let viewStore = ViewStore(Store(initialState: 0, reducer: counterReducer, environment: ())) + + var count = 0 + viewStore.publisher.producer.startWithValues { count = $0 } + + XCTAssertNoDifference(count, 0) + viewStore.send(()) + XCTAssertNoDifference(count, 1) + } +} diff --git a/Tests/ComposableArchitectureTests/ReducerTests.swift b/Tests/ComposableArchitectureTests/ReducerTests.swift index fecc4ab..9fbc941 100644 --- a/Tests/ComposableArchitectureTests/ReducerTests.swift +++ b/Tests/ComposableArchitectureTests/ReducerTests.swift @@ -1 +1,225 @@ -import Foundation +import ComposableArchitecture +import CustomDump +import XCTest +import os.signpost + +final class ReducerTests: XCTestCase { + + func testCallableAsFunction() { + let reducer = Reducer { state, _, _ in + state += 1 + return .none + } + + var state = 0 + _ = reducer.run(&state, (), ()) + XCTAssertNoDifference(state, 1) + } + + func testCombine_EffectsAreMerged() { + typealias Scheduler = DateScheduler + enum Action: Equatable { + case increment + } + + var fastValue: Int? + let fastReducer = Reducer { state, _, scheduler in + state += 1 + return Effect.fireAndForget { fastValue = 42 } + .delay(1, on: scheduler) + .eraseToEffect() + } + + var slowValue: Int? + let slowReducer = Reducer { state, _, scheduler in + state += 1 + return Effect.fireAndForget { slowValue = 1729 } + .delay(2, on: scheduler) + .eraseToEffect() + } + + let scheduler = TestScheduler() + let store = TestStore( + initialState: 0, + reducer: .combine(fastReducer, slowReducer), + environment: scheduler + ) + + store.send(.increment) { + $0 = 2 + } + // Waiting a second causes the fast effect to fire. + scheduler.advance(by: 1) + XCTAssertNoDifference(fastValue, 42) + // Waiting one more second causes the slow effect to fire. This proves that the effects + // are merged together, as opposed to concatenated. + scheduler.advance(by: 1) + XCTAssertNoDifference(slowValue, 1729) + } + + func testCombine() { + enum Action: Equatable { + case increment + } + + var childEffectExecuted = false + let childReducer = Reducer { state, _, _ in + state += 1 + return Effect.fireAndForget { childEffectExecuted = true } + .eraseToEffect() + } + + var mainEffectExecuted = false + let mainReducer = Reducer { state, _, _ in + state += 1 + return Effect.fireAndForget { mainEffectExecuted = true } + .eraseToEffect() + } + .combined(with: childReducer) + + let store = TestStore( + initialState: 0, + reducer: mainReducer, + environment: () + ) + + store.send(.increment) { + $0 = 2 + } + + XCTAssertTrue(childEffectExecuted) + XCTAssertTrue(mainEffectExecuted) + } + + func testDebug() { + var logs: [String] = [] + let logsExpectation = self.expectation(description: "logs") + logsExpectation.expectedFulfillmentCount = 2 + + let reducer = Reducer { state, action, _ in + switch action { + case .incrWithBool: + return .none + case .incr: + state.count += 1 + return .none + case .noop: + return .none + } + } + .debug("[prefix]") { _ in + DebugEnvironment( + printer: { + logs.append($0) + logsExpectation.fulfill() + } + ) + } + + let store = TestStore( + initialState: .init(), + reducer: reducer, + environment: () + ) + store.send(.incr) { $0.count = 1 } + store.send(.noop) + + self.wait(for: [logsExpectation], timeout: 2) + + XCTAssertNoDifference( + logs, + [ + #""" + [prefix]: received action: + DebugAction.incr + - DebugState(count: 0) + + DebugState(count: 1) + + """#, + #""" + [prefix]: received action: + DebugAction.noop + (No state changes) + + """#, + ] + ) + } + + func testDebug_ActionFormat_OnlyLabels() { + var logs: [String] = [] + let logsExpectation = self.expectation(description: "logs") + + let reducer = Reducer { state, action, _ in + switch action { + case let .incrWithBool(bool): + state.count += bool ? 1 : 0 + return .none + default: + return .none + } + } + .debug("[prefix]", actionFormat: .labelsOnly) { _ in + DebugEnvironment( + printer: { + logs.append($0) + logsExpectation.fulfill() + } + ) + } + + let viewStore = ViewStore( + Store( + initialState: .init(), + reducer: reducer, + environment: () + ) + ) + viewStore.send(.incrWithBool(true)) + + self.wait(for: [logsExpectation], timeout: 2) + + XCTAssertNoDifference( + logs, + [ + #""" + [prefix]: received action: + DebugAction.incrWithBool + - DebugState(count: 0) + + DebugState(count: 1) + + """# + ] + ) + } + + func testDefaultSignpost() { + let reducer = Reducer.empty.signpost(log: .default) + var n = 0 + let effect = reducer.run(&n, (), ()) + let expectation = self.expectation(description: "effect") + effect + .startWithCompleted { + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 0.1) + } + + func testDisabledSignpost() { + let reducer = Reducer.empty.signpost(log: .disabled) + var n = 0 + let effect = reducer.run(&n, (), ()) + let expectation = self.expectation(description: "effect") + effect + .startWithCompleted { + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 0.1) + } +} + +enum DebugAction: Equatable { + case incrWithBool(Bool) + case incr, noop +} +struct DebugState: Equatable { var count = 0 } diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index fecc4ab..4d6058b 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -1 +1,474 @@ -import Foundation +import XCTest + +@testable import ComposableArchitecture + +final class StoreTests: XCTestCase { + + let compositeDisposable = CompositeDisposable() + + func testCancellableIsRemovedOnImmediatelyCompletingEffect() { + let reducer = Reducer { _, _, _ in .none } + let store = Store(initialState: (), reducer: reducer, environment: ()) + + XCTAssertNoDifference(store.effectCancellables.count, 0) + + store.send(()) + + XCTAssertNoDifference(store.effectCancellables.count, 0) + } + + func testCancellableIsRemovedWhenEffectCompletes() { + let scheduler = TestScheduler() + let effect = Effect(value: ()) + .delay(1, on: scheduler) + .eraseToEffect() + + enum Action { case start, end } + + let reducer = Reducer { _, action, _ in + switch action { + case .start: + return effect.map { .end } + case .end: + return .none + } + } + let store = Store(initialState: (), reducer: reducer, environment: ()) + + XCTAssertNoDifference(store.effectCancellables.count, 0) + + store.send(.start) + + XCTAssertNoDifference(store.effectCancellables.count, 1) + + scheduler.advance(by: 2) + + XCTAssertNoDifference(store.effectCancellables.count, 0) + } + + func testScopedStoreReceivesUpdatesFromParent() { + let counterReducer = Reducer { state, _, _ in + state += 1 + return .none + } + + let parentStore = Store(initialState: 0, reducer: counterReducer, environment: ()) + let parentViewStore = ViewStore(parentStore) + let childStore = parentStore.scope(state: String.init) + + var values: [String] = [] + childStore.$state.producer + .startWithValues({ + values.append($0) + }) + + XCTAssertNoDifference(values, ["0"]) + + parentViewStore.send(()) + + XCTAssertNoDifference(values, ["0", "1"]) + } + + func testParentStoreReceivesUpdatesFromChild() { + let counterReducer = Reducer { state, _, _ in + state += 1 + return .none + } + + let parentStore = Store(initialState: 0, reducer: counterReducer, environment: ()) + let childStore = parentStore.scope(state: String.init) + let childViewStore = ViewStore(childStore) + + var values: [Int] = [] + parentStore.$state.producer + .startWithValues({ + values.append($0) + }) + + XCTAssertNoDifference(values, [0]) + + childViewStore.send(()) + + XCTAssertNoDifference(values, [0, 1]) + } + + func testScopeCallCount() { + let counterReducer = Reducer { state, _, _ in state += 1 + return .none + } + + var numCalls1 = 0 + _ = Store(initialState: 0, reducer: counterReducer, environment: ()) + .scope(state: { (count: Int) -> Int in + numCalls1 += 1 + return count + }) + + XCTAssertNoDifference(numCalls1, 1) + } + + func testScopeCallCount2() { + let counterReducer = Reducer { state, _, _ in + state += 1 + return .none + } + + var numCalls1 = 0 + var numCalls2 = 0 + var numCalls3 = 0 + + let store1 = Store(initialState: 0, reducer: counterReducer, environment: ()) + let store2 = + store1 + .scope(state: { (count: Int) -> Int in + numCalls1 += 1 + return count + }) + let store3 = + store2 + .scope(state: { (count: Int) -> Int in + numCalls2 += 1 + return count + }) + let store4 = + store3 + .scope(state: { (count: Int) -> Int in + numCalls3 += 1 + return count + }) + + _ = ViewStore(store1) + _ = ViewStore(store2) + _ = ViewStore(store3) + let viewStore4 = ViewStore(store4) + + XCTAssertNoDifference(numCalls1, 1) + XCTAssertNoDifference(numCalls2, 1) + XCTAssertNoDifference(numCalls3, 1) + + viewStore4.send(()) + + XCTAssertNoDifference(numCalls1, 2) + XCTAssertNoDifference(numCalls2, 2) + XCTAssertNoDifference(numCalls3, 2) + + viewStore4.send(()) + + XCTAssertNoDifference(numCalls1, 3) + XCTAssertNoDifference(numCalls2, 3) + XCTAssertNoDifference(numCalls3, 3) + + viewStore4.send(()) + + XCTAssertNoDifference(numCalls1, 4) + XCTAssertNoDifference(numCalls2, 4) + XCTAssertNoDifference(numCalls3, 4) + + viewStore4.send(()) + + XCTAssertNoDifference(numCalls1, 5) + XCTAssertNoDifference(numCalls2, 5) + XCTAssertNoDifference(numCalls3, 5) + } + + func testSynchronousEffectsSentAfterSinking() { + enum Action { + case tap + case next1 + case next2 + case end + } + var values: [Int] = [] + let counterReducer = Reducer { state, action, _ in + switch action { + case .tap: + return .merge( + Effect(value: .next1), + Effect(value: .next2), + .fireAndForget { values.append(1) } + ) + case .next1: + return .merge( + Effect(value: .end), + .fireAndForget { values.append(2) } + ) + case .next2: + return .fireAndForget { values.append(3) } + case .end: + return .fireAndForget { values.append(4) } + } + } + + let store = Store(initialState: (), reducer: counterReducer, environment: ()) + + store.send(.tap) + + XCTAssertNoDifference(values, [1, 2, 3, 4]) + } + + func testLotsOfSynchronousActions() { + enum Action { case incr, noop } + let reducer = Reducer { state, action, _ in + switch action { + case .incr: + state += 1 + return state >= 100_000 ? Effect(value: .noop) : Effect(value: .incr) + case .noop: + return .none + } + } + + let store = Store(initialState: 0, reducer: reducer, environment: ()) + store.send(.incr) + XCTAssertNoDifference(ViewStore(store).state, 100_000) + } + + func testIfLetAfterScope() { + struct AppState { + var count: Int? + } + + let appReducer = Reducer { state, action, _ in + state.count = action + return .none + } + + let parentStore = Store(initialState: AppState(), reducer: appReducer, environment: ()) + + // NB: This test needs to hold a strong reference to the emitted stores + var outputs: [Int?] = [] + var stores: [Any] = [] + + compositeDisposable += parentStore + .scope(state: { $0.count }) + .ifLet( + then: { store in + stores.append(store) + outputs.append(store.state) + }, + else: { + outputs.append(nil) + } + ) + + XCTAssertNoDifference(outputs, [nil]) + + parentStore.send(1) + XCTAssertNoDifference(outputs, [nil, 1]) + + parentStore.send(nil) + XCTAssertNoDifference(outputs, [nil, 1, nil]) + + parentStore.send(1) + XCTAssertNoDifference(outputs, [nil, 1, nil, 1]) + + parentStore.send(nil) + XCTAssertNoDifference(outputs, [nil, 1, nil, 1, nil]) + + parentStore.send(1) + XCTAssertNoDifference(outputs, [nil, 1, nil, 1, nil, 1]) + + parentStore.send(nil) + XCTAssertNoDifference(outputs, [nil, 1, nil, 1, nil, 1, nil]) + } + + func testIfLetTwo() { + let parentStore = Store( + initialState: 0, + reducer: Reducer { state, action, _ in + if action { + state? += 1 + return .none + } else { + return Effect(value: true).observe(on: QueueScheduler.main) + } + }, + environment: () + ) + + compositeDisposable += parentStore + .ifLet(then: { childStore in + let vs = ViewStore(childStore) + + vs + .publisher.producer + .startWithValues {_ in} + + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + vs.send(false) + _ = XCTWaiter.wait(for: [.init()], timeout: 0.1) + XCTAssertNoDifference(vs.state, 3) + }) + } + + func testActionQueuing() { + let subject = Signal.pipe() + + enum Action: Equatable { + case incrementTapped + case `init` + case doIncrement + } + + let store = TestStore( + initialState: 0, + reducer: Reducer { state, action, _ in + switch action { + case .incrementTapped: + subject.input.send(value: ()) + return .none + case .`init`: + return subject.output.producer.map { .doIncrement }.eraseToEffect() + case .doIncrement: + state += 1 + return .none + } + }, + environment: () + ) + + store.send(.`init`) + store.send(.incrementTapped) + store.receive(.doIncrement) { + $0 = 1 + } + store.send(.incrementTapped) + store.receive(.doIncrement) { + $0 = 2 + } + subject.input.sendCompleted() + } + + func testCoalesceSynchronousActions() { + let store = Store( + initialState: 0, + reducer: Reducer { state, action, _ in + switch action { + case 0: + return .merge( + Effect(value: 1), + Effect(value: 2), + Effect(value: 3) + ) + default: + state = action + return .none + } + }, + environment: () + ) + + var emissions: [Int] = [] + let viewStore = ViewStore(store) + viewStore.publisher.producer + .startWithValues{ emissions.append($0) } + + XCTAssertNoDifference(emissions, [0]) + + viewStore.send(0) + + XCTAssertNoDifference(emissions, [0, 3]) + } + + func testBufferedActionProcessing() { + struct ChildState: Equatable { + var count: Int? + } + + let childReducer = Reducer { state, action, _ in + state.count = action + return .none + } + + struct ParentState: Equatable { + var count: Int? + var child: ChildState? + } + + enum ParentAction: Equatable { + case button + case child(Int?) + } + + var handledActions: [ParentAction] = [] + let parentReducer = Reducer.combine([ + childReducer + .optional() + .pullback( + state: \.child, + action: /ParentAction.child, + environment: {} + ), + Reducer { state, action, _ in + handledActions.append(action) + + switch action { + case .button: + state.child = .init(count: nil) + return .none + + case .child(let childCount): + state.count = childCount + return .none + } + }, + ]) + + let parentStore = Store( + initialState: .init(), + reducer: parentReducer, + environment: () + ) + + compositeDisposable += parentStore + .scope( + state: \.child, + action: ParentAction.child + ) + .ifLet { childStore in + ViewStore(childStore).send(2) + } + + XCTAssertNoDifference(handledActions, []) + + parentStore.send(.button) + XCTAssertNoDifference( + handledActions, + [ + .button, + .child(2), + ]) + } + + func testNonMainQueueStore() { + var expectations: [XCTestExpectation] = [] + for i in 1...100 { + let expectation = XCTestExpectation(description: "\(i)th iteration is complete") + expectations.append(expectation) + DispatchQueue.global().async { + let viewStore = ViewStore( + Store.unchecked( + initialState: 0, + reducer: Reducer { state, _, expectation in + state += 1 + if state == 2 { + return .fireAndForget { expectation.fulfill() } + } + return .none + }, + environment: expectation + ) + ) + viewStore.send(()) + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { + viewStore.send(()) + } + } + } + + wait(for: expectations, timeout: 1) + } +} diff --git a/Tests/ComposableArchitectureTests/TestStoreTests.swift b/Tests/ComposableArchitectureTests/TestStoreTests.swift index fecc4ab..fed153c 100644 --- a/Tests/ComposableArchitectureTests/TestStoreTests.swift +++ b/Tests/ComposableArchitectureTests/TestStoreTests.swift @@ -1 +1,60 @@ -import Foundation +import ComposableArchitecture +import XCTest + +class TestStoreTests: XCTestCase { + func testEffectConcatenation() { + struct State: Equatable {} + + enum Action: Equatable { + case a, b1, b2, b3, c1, c2, c3, d + } + + let testScheduler = TestScheduler() + + let reducer = Reducer { _, action, scheduler in + switch action { + case .a: + return .merge( + Effect.concatenate(.init(value: .b1), .init(value: .c1)) + .delay(1, on: scheduler) + .eraseToEffect(), + Effect.none + .cancellable(id: 1) + ) + case .b1: + return + Effect + .concatenate(.init(value: .b2), .init(value: .b3)) + case .c1: + return + Effect + .concatenate(.init(value: .c2), .init(value: .c3)) + case .b2, .b3, .c2, .c3: + return .none + + case .d: + return .cancel(id: 1) + } + } + + let store = TestStore( + initialState: State(), + reducer: reducer, + environment: testScheduler + ) + + store.send(.a) + + testScheduler.advance(by: 1) + + store.receive(.b1) + store.receive(.b2) + store.receive(.b3) + + store.receive(.c1) + store.receive(.c2) + store.receive(.c3) + + store.send(.d) + } +} diff --git a/Tests/ComposableArchitectureTests/TimerTests.swift b/Tests/ComposableArchitectureTests/TimerTests.swift index fecc4ab..2c8fdd1 100644 --- a/Tests/ComposableArchitectureTests/TimerTests.swift +++ b/Tests/ComposableArchitectureTests/TimerTests.swift @@ -1 +1,122 @@ -import Foundation +//import Combine +//import ComposableArchitecture +//import XCTest +// +//final class TimerTests: XCTestCase { +// var cancellables: Set = [] +// +// func testTimer() { +// let scheduler = DispatchQueue.test +// +// var count = 0 +// +// Effect.timer(id: 1, every: .seconds(1), on: scheduler) +// .sink { _ in count += 1 } +// .store(in: &self.cancellables) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count, 1) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count, 2) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count, 3) +// +// scheduler.advance(by: 3) +// XCTAssertNoDifference(count, 6) +// } +// +// func testInterleavingTimer() { +// let scheduler = DispatchQueue.test +// +// var count2 = 0 +// var count3 = 0 +// +// Effect.merge( +// Effect.timer(id: 1, every: .seconds(2), on: scheduler) +// .handleEvents(receiveOutput: { _ in count2 += 1 }) +// .eraseToEffect(), +// Effect.timer(id: 2, every: .seconds(3), on: scheduler) +// .handleEvents(receiveOutput: { _ in count3 += 1 }) +// .eraseToEffect() +// ) +// .sink { _ in } +// .store(in: &self.cancellables) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count2, 0) +// XCTAssertNoDifference(count3, 0) +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count2, 1) +// XCTAssertNoDifference(count3, 0) +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count2, 1) +// XCTAssertNoDifference(count3, 1) +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count2, 2) +// XCTAssertNoDifference(count3, 1) +// } +// +// func testTimerCancellation() { +// let scheduler = DispatchQueue.test +// +// var firstCount = 0 +// var secondCount = 0 +// +// struct CancelToken: Hashable {} +// +// Effect.timer(id: CancelToken(), every: .seconds(2), on: scheduler) +// .handleEvents(receiveOutput: { _ in firstCount += 1 }) +// .eraseToEffect() +// .sink { _ in } +// .store(in: &self.cancellables) +// +// scheduler.advance(by: 2) +// +// XCTAssertNoDifference(firstCount, 1) +// +// scheduler.advance(by: 2) +// +// XCTAssertNoDifference(firstCount, 2) +// +// Effect.timer(id: CancelToken(), every: .seconds(2), on: scheduler) +// .handleEvents(receiveOutput: { _ in secondCount += 1 }) +// .eraseToEffect() +// .sink { _ in } +// .store(in: &self.cancellables) +// +// scheduler.advance(by: 2) +// +// XCTAssertNoDifference(firstCount, 2) +// XCTAssertNoDifference(secondCount, 1) +// +// scheduler.advance(by: 2) +// +// XCTAssertNoDifference(firstCount, 2) +// XCTAssertNoDifference(secondCount, 2) +// } +// +// func testTimerCompletion() { +// let scheduler = DispatchQueue.test +// +// var count = 0 +// +// Effect.timer(id: 1, every: .seconds(1), on: scheduler) +// .prefix(3) +// .sink { _ in count += 1 } +// .store(in: &self.cancellables) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count, 1) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count, 2) +// +// scheduler.advance(by: 1) +// XCTAssertNoDifference(count, 3) +// +// scheduler.run() +// XCTAssertNoDifference(count, 3) +// } +//} diff --git a/Tests/ComposableArchitectureTests/ViewStoreTests.swift b/Tests/ComposableArchitectureTests/ViewStoreTests.swift index fecc4ab..80b9bc2 100644 --- a/Tests/ComposableArchitectureTests/ViewStoreTests.swift +++ b/Tests/ComposableArchitectureTests/ViewStoreTests.swift @@ -1 +1,254 @@ -import Foundation +import ComposableArchitecture +import XCTest + +final class ViewStoreTests: XCTestCase { + + override func setUp() { + super.setUp() + equalityChecks = 0 + subEqualityChecks = 0 + } + + func testPublisherFirehose() { + let store = Store( + initialState: 0, + reducer: Reducer.empty, + environment: () + ) + + let viewStore = ViewStore(store) + + var emissionCount = 0 + viewStore.publisher.producer + .startWithValues { _ in + emissionCount += 1 + } + + XCTAssertNoDifference(emissionCount, 1) + viewStore.send(()) + XCTAssertNoDifference(emissionCount, 1) + viewStore.send(()) + XCTAssertNoDifference(emissionCount, 1) + viewStore.send(()) + XCTAssertNoDifference(emissionCount, 1) + } + + func testEqualityChecks() { + let store = Store( + initialState: State(), + reducer: Reducer.empty, + environment: () + ) + + let store1 = store.scope(state: { $0 }) + let store2 = store1.scope(state: { $0 }) + let store3 = store2.scope(state: { $0 }) + let store4 = store3.scope(state: { $0 }) + + let viewStore1 = ViewStore(store1) + let viewStore2 = ViewStore(store2) + let viewStore3 = ViewStore(store3) + let viewStore4 = ViewStore(store4) + + viewStore1.publisher.producer.startWithValues{ _ in } + viewStore2.publisher.producer.startWithValues{ _ in } + viewStore3.publisher.producer.startWithValues{ _ in } + viewStore4.publisher.producer.startWithValues{ _ in } + viewStore1.publisher.substate.producer.startWithValues{ _ in } + viewStore2.publisher.substate.producer.startWithValues{ _ in } + viewStore3.publisher.substate.producer.startWithValues{ _ in } + viewStore4.publisher.substate.producer.startWithValues{ _ in } + + XCTAssertNoDifference(0, equalityChecks) + XCTAssertNoDifference(0, subEqualityChecks) + viewStore4.send(()) + XCTAssertNoDifference(4, equalityChecks) + XCTAssertNoDifference(4, subEqualityChecks) + viewStore4.send(()) + XCTAssertNoDifference(8, equalityChecks) + XCTAssertNoDifference(8, subEqualityChecks) + viewStore4.send(()) + XCTAssertNoDifference(12, equalityChecks) + XCTAssertNoDifference(12, subEqualityChecks) + viewStore4.send(()) + XCTAssertNoDifference(16, equalityChecks) + XCTAssertNoDifference(16, subEqualityChecks) + } + + func testAccessViewStoreStateInPublisherSink() { + let reducer = Reducer { count, _, _ in + count += 1 + return .none + } + + let store = Store(initialState: 0, reducer: reducer, environment: ()) + let viewStore = ViewStore(store) + + var results: [Int] = [] + + viewStore.publisher.producer + .startWithValues({ _ in + results.append(viewStore.state) + }) + viewStore.send(()) + viewStore.send(()) + viewStore.send(()) + + XCTAssertNoDifference([0, 1, 2, 3], results) + } + + func testWillSet() { + let reducer = Reducer { count, _, _ in + count += 1 + return .none + } + + let store = Store(initialState: 0, reducer: reducer, environment: ()) + let viewStore = ViewStore(store) + + var results: [Int] = [] + + viewStore.objectWillChange.eraseToEffect() + .startWithValues({ _ in + results.append(viewStore.state) + }) + + viewStore.send(()) + viewStore.send(()) + viewStore.send(()) + + XCTAssertNoDifference([0, 1, 2], results) + } + + // MARK: TODO + func disabled_testPublisherOwnsViewStore() { + let reducer = Reducer { count, _, _ in + count += 1 + return .none + } + let store = Store(initialState: 0, reducer: reducer, environment: ()) + + var results: [Int] = [] + ViewStore(store) + .publisher.producer + .startWithValues { results.append($0) } + ViewStore(store).send(()) + print(results) + XCTAssertNoDifference(results, [0, 1]) + } + + func testStorePublisherSubscriptionOrder() { + let reducer = Reducer { count, _, _ in + count += 1 + return .none + } + let store = Store(initialState: 0, reducer: reducer, environment: ()) + let viewStore = ViewStore(store) + + var results: [Int] = [] + + viewStore.publisher.producer + .startWithValues { _ in results.append(0)} + + viewStore.publisher.producer + .startWithValues { _ in results.append(1)} + + viewStore.publisher.producer + .startWithValues { _ in results.append(2)} + + XCTAssertNoDifference(results, [0, 1, 2]) + + for _ in 0..<9 { + viewStore.send(()) + } + + XCTAssertNoDifference(results, Array(repeating: [0, 1, 2], count: 10).flatMap { $0 }) + } + + #if canImport(_Concurrency) && compiler(>=5.5.2) + func testSendWhile() { + let expectation = self.expectation(description: "await") + Task { @MainActor in + enum Action { + case response + case tapped + } + let reducer = Reducer { state, action, environment in + switch action { + case .response: + state = false + return .none + case .tapped: + state = true + return Effect(value: .response) + .observe(on: QueueScheduler.main) + .eraseToEffect() + } + } + + let store = Store(initialState: false, reducer: reducer, environment: ()) + let viewStore = ViewStore(store) + + XCTAssertNoDifference(viewStore.state, false) + await viewStore.send(.tapped, while: { $0 }) + XCTAssertNoDifference(viewStore.state, false) + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + } + + func testSuspend() { + let expectation = self.expectation(description: "await") + Task { @MainActor in + enum Action { + case response + case tapped + } + let reducer = Reducer { state, action, environment in + switch action { + case .response: + state = false + return .none + case .tapped: + state = true + return Effect(value: .response) + .observe(on: QueueScheduler.main) + .eraseToEffect() + } + } + + let store = Store(initialState: false, reducer: reducer, environment: ()) + let viewStore = ViewStore(store) + + XCTAssertNoDifference(viewStore.state, false) + viewStore.send(.tapped) + XCTAssertNoDifference(viewStore.state, true) + await viewStore.suspend(while: { $0 }) + XCTAssertNoDifference(viewStore.state, false) + expectation.fulfill() + } + self.wait(for: [expectation], timeout: 1) + } + #endif +} + +private struct State: Equatable { + var substate = Substate() + + static func == (lhs: Self, rhs: Self) -> Bool { + equalityChecks += 1 + return lhs.substate == rhs.substate + } +} + +private struct Substate: Equatable { + var name = "Blob" + + static func == (lhs: Self, rhs: Self) -> Bool { + subEqualityChecks += 1 + return lhs.name == rhs.name + } +} + +private var equalityChecks = 0 +private var subEqualityChecks = 0 diff --git a/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift b/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift index fecc4ab..90be51f 100644 --- a/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift +++ b/Tests/ComposableArchitectureTests/WithViewStoreAppTest.swift @@ -1 +1,38 @@ -import Foundation +// NB: This file gathers coverage of `WithViewStore` use as a `Scene`. + +import ComposableArchitecture +import SwiftUI + +@available(iOS 14, macOS 11, tvOS 14, watchOS 7, *) +struct TestApp: App { + let store = Store( + initialState: 0, + reducer: Reducer { state, _, _ in + state += 1 + return .none + }, + environment: () + ) + + var body: some Scene { + WithViewStore(self.store) { viewStore in + #if os(iOS) || os(macOS) + WindowGroup { + EmptyView() + } + .commands { + CommandMenu("Commands") { + Button("Increment") { + viewStore.send(()) + } + .keyboardShortcut("+") + } + } + #else + WindowGroup { + EmptyView() + } + #endif + } + } +} diff --git a/Tests/LinuxMain.swift b/Tests/LinuxMain.swift deleted file mode 100644 index 8a04d7b..0000000 --- a/Tests/LinuxMain.swift +++ /dev/null @@ -1,2 +0,0 @@ -// LinuxMain.swift -fatalError("Run the tests with `swift test --enable-test-discovery`.")