├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── Redux.Demo.SwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── Redux.Demo.SwiftUI.xcscheme ├── Redux.Demo.SwiftUI.xctestplan ├── Redux.Demo.SwiftUI ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Extensions │ ├── Combine │ │ └── Publishers+Async.swift │ ├── String+Extensions.swift │ └── SwiftUI │ │ ├── Color+SystemColors.swift │ │ └── EdgeInsets+Extensions.swift ├── Middleware │ ├── UIMiddleware.swift │ └── UserAuthMiddleware.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Reducers+Actions │ ├── AppReducer.swift │ ├── UIReducer+ColorWizard.swift │ ├── UIReducer+General.swift │ ├── UIReducer+LandingScreen.swift │ ├── UIReducer.swift │ └── UserReducer.swift ├── Redux │ ├── Redux+Middleware.swift │ ├── Redux+State.swift │ ├── Redux+Store+Enviroment.swift │ ├── Redux+Store.swift │ └── Redux.swift ├── Redux_Demo_SwiftUIApp.swift ├── Services │ └── ColorService.swift └── UI │ ├── Screens │ ├── AppRootView.swift │ ├── ColorWizard │ │ ├── ColorWizardColorView.swift │ │ ├── ColorWizardRoutesView.swift │ │ ├── ColorWizardSummaryView.swift │ │ └── Configuration │ │ │ ├── ColorWizardConfiguration+Mock.swift │ │ │ └── ColorWizardConfiguration.swift │ ├── LandingView.swift │ ├── PulseView.swift │ └── SignInCardView.swift │ ├── Styles │ ├── ButtonStyles │ │ └── BrightBorderedButtonStyle.swift │ └── TextStyles │ │ ├── ButtonTextStyle.swift │ │ └── TextStyle.swift │ └── Views │ ├── AppDependencyContainerView.swift │ ├── CardView.swift │ └── VisualEffectView.swift ├── Redux.Demo.SwiftUITests ├── Extensions │ ├── String+ExtensionsTests.swift │ └── SwiftUI │ │ └── EdgeInsets+ExtensionsTests.swift ├── Middleware │ ├── UIMiddleware │ │ ├── UIMiddlewareTest+ColorWizard.swift │ │ ├── UIMiddlewareTest+General.swift │ │ ├── UIMiddlewareTest+LandingScreen.swift │ │ └── UIMiddlewareTest.swift │ └── UserAuthMiddleware │ │ └── UserAuthMiddlewareTest.swift ├── MocksAndTestObjects │ └── ActionsCacheReducer.swift ├── Reducers+Actions │ └── UIReducer+ColorWizardTests.swift ├── Redux_Demo_SwiftUITests.swift └── Services │ └── ColorServiceTests.swift └── Redux.Demo.SwiftUIUITests ├── Redux_Demo_SwiftUIUITests.swift └── Redux_Demo_SwiftUIUITestsLaunchTests.swift /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ 91 | .DS_Store 92 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Jason Lew-Rapai 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redux.Demo.SwiftUI 2 | Demo app featuring an idealized Redux-based architecture. 3 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6F1643D62BB61F1800B9E90F /* Redux+Middleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1643D52BB61F1800B9E90F /* Redux+Middleware.swift */; }; 11 | 6F1643D82BB6209700B9E90F /* Redux+Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1643D72BB6209700B9E90F /* Redux+Store.swift */; }; 12 | 6F1643DB2BB63A6200B9E90F /* AppReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1643DA2BB63A6200B9E90F /* AppReducer.swift */; }; 13 | 6F1643DF2BB6409C00B9E90F /* UserReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F1643DE2BB6409C00B9E90F /* UserReducer.swift */; }; 14 | 6F319C9C2BBFD2B700788B29 /* ColorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F319C9B2BBFD2B700788B29 /* ColorService.swift */; }; 15 | 6F319C9F2BBFD44800788B29 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F319C9E2BBFD44800788B29 /* String+Extensions.swift */; }; 16 | 6F390B0E2BDAF7EF00F91E44 /* String+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B0D2BDAF7EF00F91E44 /* String+ExtensionsTests.swift */; }; 17 | 6F390B112BDAF8E500F91E44 /* UIMiddlewareTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B102BDAF8E500F91E44 /* UIMiddlewareTest.swift */; }; 18 | 6F390B172BDB581D00F91E44 /* UIMiddlewareTest+General.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B162BDB581D00F91E44 /* UIMiddlewareTest+General.swift */; }; 19 | 6F390B192BDB586300F91E44 /* UIMiddlewareTest+ColorWizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B182BDB586300F91E44 /* UIMiddlewareTest+ColorWizard.swift */; }; 20 | 6F390B1B2BDB5DC200F91E44 /* ActionsCacheReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B1A2BDB5DC200F91E44 /* ActionsCacheReducer.swift */; }; 21 | 6F390B1D2BDB60FD00F91E44 /* UIMiddlewareTest+LandingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B1C2BDB60FD00F91E44 /* UIMiddlewareTest+LandingScreen.swift */; }; 22 | 6F390B202BDB670C00F91E44 /* UserAuthMiddlewareTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F390B1F2BDB670C00F91E44 /* UserAuthMiddlewareTest.swift */; }; 23 | 6F4ADCE62BB52391001A8A13 /* Redux_Demo_SwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4ADCE52BB52391001A8A13 /* Redux_Demo_SwiftUIApp.swift */; }; 24 | 6F4ADCE82BB52391001A8A13 /* AppRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4ADCE72BB52391001A8A13 /* AppRootView.swift */; }; 25 | 6F4ADCEA2BB52392001A8A13 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F4ADCE92BB52392001A8A13 /* Assets.xcassets */; }; 26 | 6F4ADCED2BB52392001A8A13 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6F4ADCEC2BB52392001A8A13 /* Preview Assets.xcassets */; }; 27 | 6F4ADCF72BB52392001A8A13 /* Redux_Demo_SwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4ADCF62BB52392001A8A13 /* Redux_Demo_SwiftUITests.swift */; }; 28 | 6F4ADD012BB52392001A8A13 /* Redux_Demo_SwiftUIUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4ADD002BB52392001A8A13 /* Redux_Demo_SwiftUIUITests.swift */; }; 29 | 6F4ADD032BB52392001A8A13 /* Redux_Demo_SwiftUIUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F4ADD022BB52392001A8A13 /* Redux_Demo_SwiftUIUITestsLaunchTests.swift */; }; 30 | 6F67DC0D2BDA054F000AA1A7 /* EdgeInsets+ExtensionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F67DC0C2BDA054F000AA1A7 /* EdgeInsets+ExtensionsTests.swift */; }; 31 | 6F8F6F1D2BB526B900658766 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = 6F8F6F1C2BB526B900658766 /* CombineExt */; }; 32 | 6F8F6F232BB52E5600658766 /* Redux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F6F222BB52E5600658766 /* Redux.swift */; }; 33 | 6F8F6F252BB52E7000658766 /* Redux+State.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F8F6F242BB52E7000658766 /* Redux+State.swift */; }; 34 | 6F9B7BF42BDCBED9009E37DC /* ColorServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B7BF32BDCBED9009E37DC /* ColorServiceTests.swift */; }; 35 | 6F9B7BF72BDCC158009E37DC /* Publishers+Async.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B7BF62BDCC158009E37DC /* Publishers+Async.swift */; }; 36 | 6F9B7BFA2BDE0E97009E37DC /* UIReducer+ColorWizardTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F9B7BF92BDE0E97009E37DC /* UIReducer+ColorWizardTests.swift */; }; 37 | 6FB2178F2BB7244D002FBA7F /* Redux+Store+Enviroment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2178E2BB7244D002FBA7F /* Redux+Store+Enviroment.swift */; }; 38 | 6FB217922BB72865002FBA7F /* UserAuthMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217912BB72865002FBA7F /* UserAuthMiddleware.swift */; }; 39 | 6FB217942BB76CAB002FBA7F /* UIReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217932BB76CAB002FBA7F /* UIReducer.swift */; }; 40 | 6FB217972BBE47A5002FBA7F /* UIMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217962BBE47A5002FBA7F /* UIMiddleware.swift */; }; 41 | 6FB217992BBF406C002FBA7F /* LandingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217982BBF406C002FBA7F /* LandingView.swift */; }; 42 | 6FB2179C2BBF4210002FBA7F /* AppDependencyContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2179B2BBF4210002FBA7F /* AppDependencyContainerView.swift */; }; 43 | 6FB217A02BBF46DE002FBA7F /* BrightBorderedButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB2179F2BBF46DE002FBA7F /* BrightBorderedButtonStyle.swift */; }; 44 | 6FB217A32BBF4776002FBA7F /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217A22BBF4776002FBA7F /* TextStyle.swift */; }; 45 | 6FB217A52BBF478F002FBA7F /* ButtonTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217A42BBF478F002FBA7F /* ButtonTextStyle.swift */; }; 46 | 6FB217A72BBF9D49002FBA7F /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217A62BBF9D49002FBA7F /* CardView.swift */; }; 47 | 6FB217A92BBF9DB4002FBA7F /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217A82BBF9DB4002FBA7F /* VisualEffectView.swift */; }; 48 | 6FB217AD2BBFA340002FBA7F /* EdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217AC2BBFA340002FBA7F /* EdgeInsets+Extensions.swift */; }; 49 | 6FB217AF2BBFA39C002FBA7F /* SignInCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217AE2BBFA39C002FBA7F /* SignInCardView.swift */; }; 50 | 6FB217B12BBFA3E3002FBA7F /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB217B02BBFA3E3002FBA7F /* Color+SystemColors.swift */; }; 51 | 6FB217B42BBFB776002FBA7F /* BusyIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 6FB217B32BBFB776002FBA7F /* BusyIndicator */; }; 52 | 6FB7A3722BC9D03D009837D0 /* UIReducer+General.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB7A3712BC9D03D009837D0 /* UIReducer+General.swift */; }; 53 | 6FB7A3742BC9D090009837D0 /* UIReducer+LandingScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB7A3732BC9D090009837D0 /* UIReducer+LandingScreen.swift */; }; 54 | 6FB7A3762BC9D0CC009837D0 /* UIReducer+ColorWizard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FB7A3752BC9D0CC009837D0 /* UIReducer+ColorWizard.swift */; }; 55 | 6FCAABBB2BC0F79000B25FE8 /* PulseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCAABBA2BC0F79000B25FE8 /* PulseView.swift */; }; 56 | 6FCAABBE2BC21C1000B25FE8 /* ColorWizardRoutesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCAABBD2BC21C1000B25FE8 /* ColorWizardRoutesView.swift */; }; 57 | 6FCAABC12BC21CCD00B25FE8 /* ColorWizardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCAABC02BC21CCD00B25FE8 /* ColorWizardConfiguration.swift */; }; 58 | 6FCAABC32BC21CEE00B25FE8 /* ColorWizardConfiguration+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCAABC22BC21CEE00B25FE8 /* ColorWizardConfiguration+Mock.swift */; }; 59 | 6FCAABC52BC21D5000B25FE8 /* ColorWizardSummaryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCAABC42BC21D5000B25FE8 /* ColorWizardSummaryView.swift */; }; 60 | 6FCAABC72BC21DA700B25FE8 /* ColorWizardColorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FCAABC62BC21DA700B25FE8 /* ColorWizardColorView.swift */; }; 61 | /* End PBXBuildFile section */ 62 | 63 | /* Begin PBXContainerItemProxy section */ 64 | 6F4ADCF32BB52392001A8A13 /* PBXContainerItemProxy */ = { 65 | isa = PBXContainerItemProxy; 66 | containerPortal = 6F4ADCDA2BB52391001A8A13 /* Project object */; 67 | proxyType = 1; 68 | remoteGlobalIDString = 6F4ADCE12BB52391001A8A13; 69 | remoteInfo = Redux.Demo.SwiftUI; 70 | }; 71 | 6F4ADCFD2BB52392001A8A13 /* PBXContainerItemProxy */ = { 72 | isa = PBXContainerItemProxy; 73 | containerPortal = 6F4ADCDA2BB52391001A8A13 /* Project object */; 74 | proxyType = 1; 75 | remoteGlobalIDString = 6F4ADCE12BB52391001A8A13; 76 | remoteInfo = Redux.Demo.SwiftUI; 77 | }; 78 | /* End PBXContainerItemProxy section */ 79 | 80 | /* Begin PBXFileReference section */ 81 | 6F1643D52BB61F1800B9E90F /* Redux+Middleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Redux+Middleware.swift"; sourceTree = ""; }; 82 | 6F1643D72BB6209700B9E90F /* Redux+Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Redux+Store.swift"; sourceTree = ""; }; 83 | 6F1643DA2BB63A6200B9E90F /* AppReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppReducer.swift; sourceTree = ""; }; 84 | 6F1643DE2BB6409C00B9E90F /* UserReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserReducer.swift; sourceTree = ""; }; 85 | 6F319C9B2BBFD2B700788B29 /* ColorService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorService.swift; sourceTree = ""; }; 86 | 6F319C9E2BBFD44800788B29 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 87 | 6F390B0C2BDAF70400F91E44 /* Redux.Demo.SwiftUI.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Redux.Demo.SwiftUI.xctestplan; sourceTree = ""; }; 88 | 6F390B0D2BDAF7EF00F91E44 /* String+ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+ExtensionsTests.swift"; sourceTree = ""; }; 89 | 6F390B102BDAF8E500F91E44 /* UIMiddlewareTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMiddlewareTest.swift; sourceTree = ""; }; 90 | 6F390B162BDB581D00F91E44 /* UIMiddlewareTest+General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIMiddlewareTest+General.swift"; sourceTree = ""; }; 91 | 6F390B182BDB586300F91E44 /* UIMiddlewareTest+ColorWizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIMiddlewareTest+ColorWizard.swift"; sourceTree = ""; }; 92 | 6F390B1A2BDB5DC200F91E44 /* ActionsCacheReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionsCacheReducer.swift; sourceTree = ""; }; 93 | 6F390B1C2BDB60FD00F91E44 /* UIMiddlewareTest+LandingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIMiddlewareTest+LandingScreen.swift"; sourceTree = ""; }; 94 | 6F390B1F2BDB670C00F91E44 /* UserAuthMiddlewareTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAuthMiddlewareTest.swift; sourceTree = ""; }; 95 | 6F4ADCE22BB52391001A8A13 /* Redux.Demo.SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Redux.Demo.SwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 96 | 6F4ADCE52BB52391001A8A13 /* Redux_Demo_SwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redux_Demo_SwiftUIApp.swift; sourceTree = ""; }; 97 | 6F4ADCE72BB52391001A8A13 /* AppRootView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootView.swift; sourceTree = ""; }; 98 | 6F4ADCE92BB52392001A8A13 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 99 | 6F4ADCEC2BB52392001A8A13 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 100 | 6F4ADCF22BB52392001A8A13 /* Redux.Demo.SwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Redux.Demo.SwiftUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 101 | 6F4ADCF62BB52392001A8A13 /* Redux_Demo_SwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redux_Demo_SwiftUITests.swift; sourceTree = ""; }; 102 | 6F4ADCFC2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Redux.Demo.SwiftUIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 103 | 6F4ADD002BB52392001A8A13 /* Redux_Demo_SwiftUIUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redux_Demo_SwiftUIUITests.swift; sourceTree = ""; }; 104 | 6F4ADD022BB52392001A8A13 /* Redux_Demo_SwiftUIUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redux_Demo_SwiftUIUITestsLaunchTests.swift; sourceTree = ""; }; 105 | 6F67DC0C2BDA054F000AA1A7 /* EdgeInsets+ExtensionsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EdgeInsets+ExtensionsTests.swift"; sourceTree = ""; }; 106 | 6F8F6F222BB52E5600658766 /* Redux.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Redux.swift; sourceTree = ""; }; 107 | 6F8F6F242BB52E7000658766 /* Redux+State.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Redux+State.swift"; sourceTree = ""; }; 108 | 6F9B7BF32BDCBED9009E37DC /* ColorServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorServiceTests.swift; sourceTree = ""; }; 109 | 6F9B7BF62BDCC158009E37DC /* Publishers+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publishers+Async.swift"; sourceTree = ""; }; 110 | 6F9B7BF92BDE0E97009E37DC /* UIReducer+ColorWizardTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIReducer+ColorWizardTests.swift"; sourceTree = ""; }; 111 | 6FB2178E2BB7244D002FBA7F /* Redux+Store+Enviroment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Redux+Store+Enviroment.swift"; sourceTree = ""; }; 112 | 6FB217912BB72865002FBA7F /* UserAuthMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAuthMiddleware.swift; sourceTree = ""; }; 113 | 6FB217932BB76CAB002FBA7F /* UIReducer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIReducer.swift; sourceTree = ""; }; 114 | 6FB217962BBE47A5002FBA7F /* UIMiddleware.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIMiddleware.swift; sourceTree = ""; }; 115 | 6FB217982BBF406C002FBA7F /* LandingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingView.swift; sourceTree = ""; }; 116 | 6FB2179B2BBF4210002FBA7F /* AppDependencyContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDependencyContainerView.swift; sourceTree = ""; }; 117 | 6FB2179F2BBF46DE002FBA7F /* BrightBorderedButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightBorderedButtonStyle.swift; sourceTree = ""; }; 118 | 6FB217A22BBF4776002FBA7F /* TextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; 119 | 6FB217A42BBF478F002FBA7F /* ButtonTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTextStyle.swift; sourceTree = ""; }; 120 | 6FB217A62BBF9D49002FBA7F /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 121 | 6FB217A82BBF9DB4002FBA7F /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; 122 | 6FB217AC2BBFA340002FBA7F /* EdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EdgeInsets+Extensions.swift"; sourceTree = ""; }; 123 | 6FB217AE2BBFA39C002FBA7F /* SignInCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInCardView.swift; sourceTree = ""; }; 124 | 6FB217B02BBFA3E3002FBA7F /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = ""; }; 125 | 6FB7A3712BC9D03D009837D0 /* UIReducer+General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIReducer+General.swift"; sourceTree = ""; }; 126 | 6FB7A3732BC9D090009837D0 /* UIReducer+LandingScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIReducer+LandingScreen.swift"; sourceTree = ""; }; 127 | 6FB7A3752BC9D0CC009837D0 /* UIReducer+ColorWizard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIReducer+ColorWizard.swift"; sourceTree = ""; }; 128 | 6FCAABBA2BC0F79000B25FE8 /* PulseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulseView.swift; sourceTree = ""; }; 129 | 6FCAABBD2BC21C1000B25FE8 /* ColorWizardRoutesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardRoutesView.swift; sourceTree = ""; }; 130 | 6FCAABC02BC21CCD00B25FE8 /* ColorWizardConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardConfiguration.swift; sourceTree = ""; }; 131 | 6FCAABC22BC21CEE00B25FE8 /* ColorWizardConfiguration+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColorWizardConfiguration+Mock.swift"; sourceTree = ""; }; 132 | 6FCAABC42BC21D5000B25FE8 /* ColorWizardSummaryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardSummaryView.swift; sourceTree = ""; }; 133 | 6FCAABC62BC21DA700B25FE8 /* ColorWizardColorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardColorView.swift; sourceTree = ""; }; 134 | /* End PBXFileReference section */ 135 | 136 | /* Begin PBXFrameworksBuildPhase section */ 137 | 6F4ADCDF2BB52391001A8A13 /* Frameworks */ = { 138 | isa = PBXFrameworksBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | 6F8F6F1D2BB526B900658766 /* CombineExt in Frameworks */, 142 | 6FB217B42BBFB776002FBA7F /* BusyIndicator in Frameworks */, 143 | ); 144 | runOnlyForDeploymentPostprocessing = 0; 145 | }; 146 | 6F4ADCEF2BB52392001A8A13 /* Frameworks */ = { 147 | isa = PBXFrameworksBuildPhase; 148 | buildActionMask = 2147483647; 149 | files = ( 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | }; 153 | 6F4ADCF92BB52392001A8A13 /* Frameworks */ = { 154 | isa = PBXFrameworksBuildPhase; 155 | buildActionMask = 2147483647; 156 | files = ( 157 | ); 158 | runOnlyForDeploymentPostprocessing = 0; 159 | }; 160 | /* End PBXFrameworksBuildPhase section */ 161 | 162 | /* Begin PBXGroup section */ 163 | 6F1643D92BB63A4F00B9E90F /* Reducers+Actions */ = { 164 | isa = PBXGroup; 165 | children = ( 166 | 6F1643DA2BB63A6200B9E90F /* AppReducer.swift */, 167 | 6FB217932BB76CAB002FBA7F /* UIReducer.swift */, 168 | 6FB7A3752BC9D0CC009837D0 /* UIReducer+ColorWizard.swift */, 169 | 6FB7A3712BC9D03D009837D0 /* UIReducer+General.swift */, 170 | 6FB7A3732BC9D090009837D0 /* UIReducer+LandingScreen.swift */, 171 | 6F1643DE2BB6409C00B9E90F /* UserReducer.swift */, 172 | ); 173 | path = "Reducers+Actions"; 174 | sourceTree = ""; 175 | }; 176 | 6F319C9A2BBFD23900788B29 /* Services */ = { 177 | isa = PBXGroup; 178 | children = ( 179 | 6F319C9B2BBFD2B700788B29 /* ColorService.swift */, 180 | ); 181 | path = Services; 182 | sourceTree = ""; 183 | }; 184 | 6F319C9D2BBFD41000788B29 /* Screens */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 6FCAABBC2BC1F00000B25FE8 /* ColorWizard */, 188 | 6F4ADCE72BB52391001A8A13 /* AppRootView.swift */, 189 | 6FB217982BBF406C002FBA7F /* LandingView.swift */, 190 | 6FCAABBA2BC0F79000B25FE8 /* PulseView.swift */, 191 | 6FB217AE2BBFA39C002FBA7F /* SignInCardView.swift */, 192 | ); 193 | path = Screens; 194 | sourceTree = ""; 195 | }; 196 | 6F390B0F2BDAF8D300F91E44 /* Middleware */ = { 197 | isa = PBXGroup; 198 | children = ( 199 | 6F390B152BDB57FF00F91E44 /* UIMiddleware */, 200 | 6F390B1E2BDB66FA00F91E44 /* UserAuthMiddleware */, 201 | ); 202 | path = Middleware; 203 | sourceTree = ""; 204 | }; 205 | 6F390B122BDAFA8100F91E44 /* MocksAndTestObjects */ = { 206 | isa = PBXGroup; 207 | children = ( 208 | 6F390B1A2BDB5DC200F91E44 /* ActionsCacheReducer.swift */, 209 | ); 210 | path = MocksAndTestObjects; 211 | sourceTree = ""; 212 | }; 213 | 6F390B152BDB57FF00F91E44 /* UIMiddleware */ = { 214 | isa = PBXGroup; 215 | children = ( 216 | 6F390B102BDAF8E500F91E44 /* UIMiddlewareTest.swift */, 217 | 6F390B162BDB581D00F91E44 /* UIMiddlewareTest+General.swift */, 218 | 6F390B182BDB586300F91E44 /* UIMiddlewareTest+ColorWizard.swift */, 219 | 6F390B1C2BDB60FD00F91E44 /* UIMiddlewareTest+LandingScreen.swift */, 220 | ); 221 | path = UIMiddleware; 222 | sourceTree = ""; 223 | }; 224 | 6F390B1E2BDB66FA00F91E44 /* UserAuthMiddleware */ = { 225 | isa = PBXGroup; 226 | children = ( 227 | 6F390B1F2BDB670C00F91E44 /* UserAuthMiddlewareTest.swift */, 228 | ); 229 | path = UserAuthMiddleware; 230 | sourceTree = ""; 231 | }; 232 | 6F4ADCD92BB52391001A8A13 = { 233 | isa = PBXGroup; 234 | children = ( 235 | 6F390B0C2BDAF70400F91E44 /* Redux.Demo.SwiftUI.xctestplan */, 236 | 6F4ADCE42BB52391001A8A13 /* Redux.Demo.SwiftUI */, 237 | 6F4ADCF52BB52392001A8A13 /* Redux.Demo.SwiftUITests */, 238 | 6F4ADCFF2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests */, 239 | 6F4ADCE32BB52391001A8A13 /* Products */, 240 | ); 241 | indentWidth = 2; 242 | sourceTree = ""; 243 | tabWidth = 2; 244 | }; 245 | 6F4ADCE32BB52391001A8A13 /* Products */ = { 246 | isa = PBXGroup; 247 | children = ( 248 | 6F4ADCE22BB52391001A8A13 /* Redux.Demo.SwiftUI.app */, 249 | 6F4ADCF22BB52392001A8A13 /* Redux.Demo.SwiftUITests.xctest */, 250 | 6F4ADCFC2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests.xctest */, 251 | ); 252 | name = Products; 253 | sourceTree = ""; 254 | }; 255 | 6F4ADCE42BB52391001A8A13 /* Redux.Demo.SwiftUI */ = { 256 | isa = PBXGroup; 257 | children = ( 258 | 6FB217AA2BBFA32B002FBA7F /* Extensions */, 259 | 6FB217902BB72853002FBA7F /* Middleware */, 260 | 6F1643D92BB63A4F00B9E90F /* Reducers+Actions */, 261 | 6F8F6F212BB52DB900658766 /* Redux */, 262 | 6F319C9A2BBFD23900788B29 /* Services */, 263 | 6FB217952BBE1D60002FBA7F /* UI */, 264 | 6F4ADCE52BB52391001A8A13 /* Redux_Demo_SwiftUIApp.swift */, 265 | 6F4ADCE92BB52392001A8A13 /* Assets.xcassets */, 266 | 6F4ADCEB2BB52392001A8A13 /* Preview Content */, 267 | ); 268 | path = Redux.Demo.SwiftUI; 269 | sourceTree = ""; 270 | }; 271 | 6F4ADCEB2BB52392001A8A13 /* Preview Content */ = { 272 | isa = PBXGroup; 273 | children = ( 274 | 6F4ADCEC2BB52392001A8A13 /* Preview Assets.xcassets */, 275 | ); 276 | path = "Preview Content"; 277 | sourceTree = ""; 278 | }; 279 | 6F4ADCF52BB52392001A8A13 /* Redux.Demo.SwiftUITests */ = { 280 | isa = PBXGroup; 281 | children = ( 282 | 6F67DC0A2BDA0520000AA1A7 /* Extensions */, 283 | 6F390B0F2BDAF8D300F91E44 /* Middleware */, 284 | 6F390B122BDAFA8100F91E44 /* MocksAndTestObjects */, 285 | 6F9B7BF82BDE0E80009E37DC /* Reducers+Actions */, 286 | 6F9B7BF22BDCBB16009E37DC /* Services */, 287 | 6F4ADCF62BB52392001A8A13 /* Redux_Demo_SwiftUITests.swift */, 288 | ); 289 | path = Redux.Demo.SwiftUITests; 290 | sourceTree = ""; 291 | }; 292 | 6F4ADCFF2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests */ = { 293 | isa = PBXGroup; 294 | children = ( 295 | 6F4ADD002BB52392001A8A13 /* Redux_Demo_SwiftUIUITests.swift */, 296 | 6F4ADD022BB52392001A8A13 /* Redux_Demo_SwiftUIUITestsLaunchTests.swift */, 297 | ); 298 | path = Redux.Demo.SwiftUIUITests; 299 | sourceTree = ""; 300 | }; 301 | 6F67DC0A2BDA0520000AA1A7 /* Extensions */ = { 302 | isa = PBXGroup; 303 | children = ( 304 | 6F67DC0B2BDA0532000AA1A7 /* SwiftUI */, 305 | 6F390B0D2BDAF7EF00F91E44 /* String+ExtensionsTests.swift */, 306 | ); 307 | path = Extensions; 308 | sourceTree = ""; 309 | }; 310 | 6F67DC0B2BDA0532000AA1A7 /* SwiftUI */ = { 311 | isa = PBXGroup; 312 | children = ( 313 | 6F67DC0C2BDA054F000AA1A7 /* EdgeInsets+ExtensionsTests.swift */, 314 | ); 315 | path = SwiftUI; 316 | sourceTree = ""; 317 | }; 318 | 6F8F6F212BB52DB900658766 /* Redux */ = { 319 | isa = PBXGroup; 320 | children = ( 321 | 6F8F6F222BB52E5600658766 /* Redux.swift */, 322 | 6F1643D52BB61F1800B9E90F /* Redux+Middleware.swift */, 323 | 6F8F6F242BB52E7000658766 /* Redux+State.swift */, 324 | 6F1643D72BB6209700B9E90F /* Redux+Store.swift */, 325 | 6FB2178E2BB7244D002FBA7F /* Redux+Store+Enviroment.swift */, 326 | ); 327 | path = Redux; 328 | sourceTree = ""; 329 | }; 330 | 6F9B7BF22BDCBB16009E37DC /* Services */ = { 331 | isa = PBXGroup; 332 | children = ( 333 | 6F9B7BF32BDCBED9009E37DC /* ColorServiceTests.swift */, 334 | ); 335 | path = Services; 336 | sourceTree = ""; 337 | }; 338 | 6F9B7BF52BDCC149009E37DC /* Combine */ = { 339 | isa = PBXGroup; 340 | children = ( 341 | 6F9B7BF62BDCC158009E37DC /* Publishers+Async.swift */, 342 | ); 343 | path = Combine; 344 | sourceTree = ""; 345 | }; 346 | 6F9B7BF82BDE0E80009E37DC /* Reducers+Actions */ = { 347 | isa = PBXGroup; 348 | children = ( 349 | 6F9B7BF92BDE0E97009E37DC /* UIReducer+ColorWizardTests.swift */, 350 | ); 351 | path = "Reducers+Actions"; 352 | sourceTree = ""; 353 | }; 354 | 6FB217902BB72853002FBA7F /* Middleware */ = { 355 | isa = PBXGroup; 356 | children = ( 357 | 6FB217962BBE47A5002FBA7F /* UIMiddleware.swift */, 358 | 6FB217912BB72865002FBA7F /* UserAuthMiddleware.swift */, 359 | ); 360 | path = Middleware; 361 | sourceTree = ""; 362 | }; 363 | 6FB217952BBE1D60002FBA7F /* UI */ = { 364 | isa = PBXGroup; 365 | children = ( 366 | 6F319C9D2BBFD41000788B29 /* Screens */, 367 | 6FB2179D2BBF46C2002FBA7F /* Styles */, 368 | 6FB2179A2BBF41F3002FBA7F /* Views */, 369 | ); 370 | path = UI; 371 | sourceTree = ""; 372 | }; 373 | 6FB2179A2BBF41F3002FBA7F /* Views */ = { 374 | isa = PBXGroup; 375 | children = ( 376 | 6FB2179B2BBF4210002FBA7F /* AppDependencyContainerView.swift */, 377 | 6FB217A62BBF9D49002FBA7F /* CardView.swift */, 378 | 6FB217A82BBF9DB4002FBA7F /* VisualEffectView.swift */, 379 | ); 380 | path = Views; 381 | sourceTree = ""; 382 | }; 383 | 6FB2179D2BBF46C2002FBA7F /* Styles */ = { 384 | isa = PBXGroup; 385 | children = ( 386 | 6FB2179E2BBF46C8002FBA7F /* ButtonStyles */, 387 | 6FB217A12BBF4700002FBA7F /* TextStyles */, 388 | ); 389 | path = Styles; 390 | sourceTree = ""; 391 | }; 392 | 6FB2179E2BBF46C8002FBA7F /* ButtonStyles */ = { 393 | isa = PBXGroup; 394 | children = ( 395 | 6FB2179F2BBF46DE002FBA7F /* BrightBorderedButtonStyle.swift */, 396 | ); 397 | path = ButtonStyles; 398 | sourceTree = ""; 399 | }; 400 | 6FB217A12BBF4700002FBA7F /* TextStyles */ = { 401 | isa = PBXGroup; 402 | children = ( 403 | 6FB217A22BBF4776002FBA7F /* TextStyle.swift */, 404 | 6FB217A42BBF478F002FBA7F /* ButtonTextStyle.swift */, 405 | ); 406 | path = TextStyles; 407 | sourceTree = ""; 408 | }; 409 | 6FB217AA2BBFA32B002FBA7F /* Extensions */ = { 410 | isa = PBXGroup; 411 | children = ( 412 | 6F9B7BF52BDCC149009E37DC /* Combine */, 413 | 6FB217AB2BBFA332002FBA7F /* SwiftUI */, 414 | 6F319C9E2BBFD44800788B29 /* String+Extensions.swift */, 415 | ); 416 | path = Extensions; 417 | sourceTree = ""; 418 | }; 419 | 6FB217AB2BBFA332002FBA7F /* SwiftUI */ = { 420 | isa = PBXGroup; 421 | children = ( 422 | 6FB217AC2BBFA340002FBA7F /* EdgeInsets+Extensions.swift */, 423 | 6FB217B02BBFA3E3002FBA7F /* Color+SystemColors.swift */, 424 | ); 425 | path = SwiftUI; 426 | sourceTree = ""; 427 | }; 428 | 6FCAABBC2BC1F00000B25FE8 /* ColorWizard */ = { 429 | isa = PBXGroup; 430 | children = ( 431 | 6FCAABBF2BC21CAA00B25FE8 /* Configuration */, 432 | 6FCAABC62BC21DA700B25FE8 /* ColorWizardColorView.swift */, 433 | 6FCAABBD2BC21C1000B25FE8 /* ColorWizardRoutesView.swift */, 434 | 6FCAABC42BC21D5000B25FE8 /* ColorWizardSummaryView.swift */, 435 | ); 436 | path = ColorWizard; 437 | sourceTree = ""; 438 | }; 439 | 6FCAABBF2BC21CAA00B25FE8 /* Configuration */ = { 440 | isa = PBXGroup; 441 | children = ( 442 | 6FCAABC02BC21CCD00B25FE8 /* ColorWizardConfiguration.swift */, 443 | 6FCAABC22BC21CEE00B25FE8 /* ColorWizardConfiguration+Mock.swift */, 444 | ); 445 | path = Configuration; 446 | sourceTree = ""; 447 | }; 448 | /* End PBXGroup section */ 449 | 450 | /* Begin PBXNativeTarget section */ 451 | 6F4ADCE12BB52391001A8A13 /* Redux.Demo.SwiftUI */ = { 452 | isa = PBXNativeTarget; 453 | buildConfigurationList = 6F4ADD062BB52392001A8A13 /* Build configuration list for PBXNativeTarget "Redux.Demo.SwiftUI" */; 454 | buildPhases = ( 455 | 6F4ADCDE2BB52391001A8A13 /* Sources */, 456 | 6F4ADCDF2BB52391001A8A13 /* Frameworks */, 457 | 6F4ADCE02BB52391001A8A13 /* Resources */, 458 | ); 459 | buildRules = ( 460 | ); 461 | dependencies = ( 462 | ); 463 | name = Redux.Demo.SwiftUI; 464 | packageProductDependencies = ( 465 | 6F8F6F1C2BB526B900658766 /* CombineExt */, 466 | 6FB217B32BBFB776002FBA7F /* BusyIndicator */, 467 | ); 468 | productName = Redux.Demo.SwiftUI; 469 | productReference = 6F4ADCE22BB52391001A8A13 /* Redux.Demo.SwiftUI.app */; 470 | productType = "com.apple.product-type.application"; 471 | }; 472 | 6F4ADCF12BB52392001A8A13 /* Redux.Demo.SwiftUITests */ = { 473 | isa = PBXNativeTarget; 474 | buildConfigurationList = 6F4ADD092BB52392001A8A13 /* Build configuration list for PBXNativeTarget "Redux.Demo.SwiftUITests" */; 475 | buildPhases = ( 476 | 6F4ADCEE2BB52392001A8A13 /* Sources */, 477 | 6F4ADCEF2BB52392001A8A13 /* Frameworks */, 478 | 6F4ADCF02BB52392001A8A13 /* Resources */, 479 | ); 480 | buildRules = ( 481 | ); 482 | dependencies = ( 483 | 6F4ADCF42BB52392001A8A13 /* PBXTargetDependency */, 484 | ); 485 | name = Redux.Demo.SwiftUITests; 486 | productName = Redux.Demo.SwiftUITests; 487 | productReference = 6F4ADCF22BB52392001A8A13 /* Redux.Demo.SwiftUITests.xctest */; 488 | productType = "com.apple.product-type.bundle.unit-test"; 489 | }; 490 | 6F4ADCFB2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests */ = { 491 | isa = PBXNativeTarget; 492 | buildConfigurationList = 6F4ADD0C2BB52392001A8A13 /* Build configuration list for PBXNativeTarget "Redux.Demo.SwiftUIUITests" */; 493 | buildPhases = ( 494 | 6F4ADCF82BB52392001A8A13 /* Sources */, 495 | 6F4ADCF92BB52392001A8A13 /* Frameworks */, 496 | 6F4ADCFA2BB52392001A8A13 /* Resources */, 497 | ); 498 | buildRules = ( 499 | ); 500 | dependencies = ( 501 | 6F4ADCFE2BB52392001A8A13 /* PBXTargetDependency */, 502 | ); 503 | name = Redux.Demo.SwiftUIUITests; 504 | productName = Redux.Demo.SwiftUIUITests; 505 | productReference = 6F4ADCFC2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests.xctest */; 506 | productType = "com.apple.product-type.bundle.ui-testing"; 507 | }; 508 | /* End PBXNativeTarget section */ 509 | 510 | /* Begin PBXProject section */ 511 | 6F4ADCDA2BB52391001A8A13 /* Project object */ = { 512 | isa = PBXProject; 513 | attributes = { 514 | BuildIndependentTargetsInParallel = 1; 515 | LastSwiftUpdateCheck = 1500; 516 | LastUpgradeCheck = 1500; 517 | TargetAttributes = { 518 | 6F4ADCE12BB52391001A8A13 = { 519 | CreatedOnToolsVersion = 15.0.1; 520 | }; 521 | 6F4ADCF12BB52392001A8A13 = { 522 | CreatedOnToolsVersion = 15.0.1; 523 | TestTargetID = 6F4ADCE12BB52391001A8A13; 524 | }; 525 | 6F4ADCFB2BB52392001A8A13 = { 526 | CreatedOnToolsVersion = 15.0.1; 527 | TestTargetID = 6F4ADCE12BB52391001A8A13; 528 | }; 529 | }; 530 | }; 531 | buildConfigurationList = 6F4ADCDD2BB52391001A8A13 /* Build configuration list for PBXProject "Redux.Demo.SwiftUI" */; 532 | compatibilityVersion = "Xcode 14.0"; 533 | developmentRegion = en; 534 | hasScannedForEncodings = 0; 535 | knownRegions = ( 536 | en, 537 | Base, 538 | ); 539 | mainGroup = 6F4ADCD92BB52391001A8A13; 540 | packageReferences = ( 541 | 6F8F6F1B2BB526B900658766 /* XCRemoteSwiftPackageReference "CombineExt" */, 542 | 6FB217B22BBFB776002FBA7F /* XCRemoteSwiftPackageReference "BusyIndicator" */, 543 | ); 544 | productRefGroup = 6F4ADCE32BB52391001A8A13 /* Products */; 545 | projectDirPath = ""; 546 | projectRoot = ""; 547 | targets = ( 548 | 6F4ADCE12BB52391001A8A13 /* Redux.Demo.SwiftUI */, 549 | 6F4ADCF12BB52392001A8A13 /* Redux.Demo.SwiftUITests */, 550 | 6F4ADCFB2BB52392001A8A13 /* Redux.Demo.SwiftUIUITests */, 551 | ); 552 | }; 553 | /* End PBXProject section */ 554 | 555 | /* Begin PBXResourcesBuildPhase section */ 556 | 6F4ADCE02BB52391001A8A13 /* Resources */ = { 557 | isa = PBXResourcesBuildPhase; 558 | buildActionMask = 2147483647; 559 | files = ( 560 | 6F4ADCED2BB52392001A8A13 /* Preview Assets.xcassets in Resources */, 561 | 6F4ADCEA2BB52392001A8A13 /* Assets.xcassets in Resources */, 562 | ); 563 | runOnlyForDeploymentPostprocessing = 0; 564 | }; 565 | 6F4ADCF02BB52392001A8A13 /* Resources */ = { 566 | isa = PBXResourcesBuildPhase; 567 | buildActionMask = 2147483647; 568 | files = ( 569 | ); 570 | runOnlyForDeploymentPostprocessing = 0; 571 | }; 572 | 6F4ADCFA2BB52392001A8A13 /* Resources */ = { 573 | isa = PBXResourcesBuildPhase; 574 | buildActionMask = 2147483647; 575 | files = ( 576 | ); 577 | runOnlyForDeploymentPostprocessing = 0; 578 | }; 579 | /* End PBXResourcesBuildPhase section */ 580 | 581 | /* Begin PBXSourcesBuildPhase section */ 582 | 6F4ADCDE2BB52391001A8A13 /* Sources */ = { 583 | isa = PBXSourcesBuildPhase; 584 | buildActionMask = 2147483647; 585 | files = ( 586 | 6FB217AD2BBFA340002FBA7F /* EdgeInsets+Extensions.swift in Sources */, 587 | 6F9B7BF72BDCC158009E37DC /* Publishers+Async.swift in Sources */, 588 | 6FB217A32BBF4776002FBA7F /* TextStyle.swift in Sources */, 589 | 6FB2179C2BBF4210002FBA7F /* AppDependencyContainerView.swift in Sources */, 590 | 6FB217942BB76CAB002FBA7F /* UIReducer.swift in Sources */, 591 | 6FCAABC52BC21D5000B25FE8 /* ColorWizardSummaryView.swift in Sources */, 592 | 6F319C9F2BBFD44800788B29 /* String+Extensions.swift in Sources */, 593 | 6FCAABC12BC21CCD00B25FE8 /* ColorWizardConfiguration.swift in Sources */, 594 | 6FB217A02BBF46DE002FBA7F /* BrightBorderedButtonStyle.swift in Sources */, 595 | 6FB7A3742BC9D090009837D0 /* UIReducer+LandingScreen.swift in Sources */, 596 | 6FB217AF2BBFA39C002FBA7F /* SignInCardView.swift in Sources */, 597 | 6FB217B12BBFA3E3002FBA7F /* Color+SystemColors.swift in Sources */, 598 | 6FB217992BBF406C002FBA7F /* LandingView.swift in Sources */, 599 | 6FCAABBE2BC21C1000B25FE8 /* ColorWizardRoutesView.swift in Sources */, 600 | 6FB217922BB72865002FBA7F /* UserAuthMiddleware.swift in Sources */, 601 | 6F8F6F252BB52E7000658766 /* Redux+State.swift in Sources */, 602 | 6F4ADCE82BB52391001A8A13 /* AppRootView.swift in Sources */, 603 | 6FB7A3762BC9D0CC009837D0 /* UIReducer+ColorWizard.swift in Sources */, 604 | 6F1643DB2BB63A6200B9E90F /* AppReducer.swift in Sources */, 605 | 6F1643DF2BB6409C00B9E90F /* UserReducer.swift in Sources */, 606 | 6FB217A72BBF9D49002FBA7F /* CardView.swift in Sources */, 607 | 6F1643D82BB6209700B9E90F /* Redux+Store.swift in Sources */, 608 | 6FB217A52BBF478F002FBA7F /* ButtonTextStyle.swift in Sources */, 609 | 6FCAABC32BC21CEE00B25FE8 /* ColorWizardConfiguration+Mock.swift in Sources */, 610 | 6F4ADCE62BB52391001A8A13 /* Redux_Demo_SwiftUIApp.swift in Sources */, 611 | 6F1643D62BB61F1800B9E90F /* Redux+Middleware.swift in Sources */, 612 | 6FB217972BBE47A5002FBA7F /* UIMiddleware.swift in Sources */, 613 | 6F8F6F232BB52E5600658766 /* Redux.swift in Sources */, 614 | 6F319C9C2BBFD2B700788B29 /* ColorService.swift in Sources */, 615 | 6FB7A3722BC9D03D009837D0 /* UIReducer+General.swift in Sources */, 616 | 6FB217A92BBF9DB4002FBA7F /* VisualEffectView.swift in Sources */, 617 | 6FCAABC72BC21DA700B25FE8 /* ColorWizardColorView.swift in Sources */, 618 | 6FB2178F2BB7244D002FBA7F /* Redux+Store+Enviroment.swift in Sources */, 619 | 6FCAABBB2BC0F79000B25FE8 /* PulseView.swift in Sources */, 620 | ); 621 | runOnlyForDeploymentPostprocessing = 0; 622 | }; 623 | 6F4ADCEE2BB52392001A8A13 /* Sources */ = { 624 | isa = PBXSourcesBuildPhase; 625 | buildActionMask = 2147483647; 626 | files = ( 627 | 6F67DC0D2BDA054F000AA1A7 /* EdgeInsets+ExtensionsTests.swift in Sources */, 628 | 6F9B7BF42BDCBED9009E37DC /* ColorServiceTests.swift in Sources */, 629 | 6F390B112BDAF8E500F91E44 /* UIMiddlewareTest.swift in Sources */, 630 | 6F4ADCF72BB52392001A8A13 /* Redux_Demo_SwiftUITests.swift in Sources */, 631 | 6F9B7BFA2BDE0E97009E37DC /* UIReducer+ColorWizardTests.swift in Sources */, 632 | 6F390B202BDB670C00F91E44 /* UserAuthMiddlewareTest.swift in Sources */, 633 | 6F390B0E2BDAF7EF00F91E44 /* String+ExtensionsTests.swift in Sources */, 634 | 6F390B192BDB586300F91E44 /* UIMiddlewareTest+ColorWizard.swift in Sources */, 635 | 6F390B1B2BDB5DC200F91E44 /* ActionsCacheReducer.swift in Sources */, 636 | 6F390B1D2BDB60FD00F91E44 /* UIMiddlewareTest+LandingScreen.swift in Sources */, 637 | 6F390B172BDB581D00F91E44 /* UIMiddlewareTest+General.swift in Sources */, 638 | ); 639 | runOnlyForDeploymentPostprocessing = 0; 640 | }; 641 | 6F4ADCF82BB52392001A8A13 /* Sources */ = { 642 | isa = PBXSourcesBuildPhase; 643 | buildActionMask = 2147483647; 644 | files = ( 645 | 6F4ADD032BB52392001A8A13 /* Redux_Demo_SwiftUIUITestsLaunchTests.swift in Sources */, 646 | 6F4ADD012BB52392001A8A13 /* Redux_Demo_SwiftUIUITests.swift in Sources */, 647 | ); 648 | runOnlyForDeploymentPostprocessing = 0; 649 | }; 650 | /* End PBXSourcesBuildPhase section */ 651 | 652 | /* Begin PBXTargetDependency section */ 653 | 6F4ADCF42BB52392001A8A13 /* PBXTargetDependency */ = { 654 | isa = PBXTargetDependency; 655 | target = 6F4ADCE12BB52391001A8A13 /* Redux.Demo.SwiftUI */; 656 | targetProxy = 6F4ADCF32BB52392001A8A13 /* PBXContainerItemProxy */; 657 | }; 658 | 6F4ADCFE2BB52392001A8A13 /* PBXTargetDependency */ = { 659 | isa = PBXTargetDependency; 660 | target = 6F4ADCE12BB52391001A8A13 /* Redux.Demo.SwiftUI */; 661 | targetProxy = 6F4ADCFD2BB52392001A8A13 /* PBXContainerItemProxy */; 662 | }; 663 | /* End PBXTargetDependency section */ 664 | 665 | /* Begin XCBuildConfiguration section */ 666 | 6F4ADD042BB52392001A8A13 /* Debug */ = { 667 | isa = XCBuildConfiguration; 668 | buildSettings = { 669 | ALWAYS_SEARCH_USER_PATHS = NO; 670 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 671 | CLANG_ANALYZER_NONNULL = YES; 672 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 673 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 674 | CLANG_ENABLE_MODULES = YES; 675 | CLANG_ENABLE_OBJC_ARC = YES; 676 | CLANG_ENABLE_OBJC_WEAK = YES; 677 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 678 | CLANG_WARN_BOOL_CONVERSION = YES; 679 | CLANG_WARN_COMMA = YES; 680 | CLANG_WARN_CONSTANT_CONVERSION = YES; 681 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 682 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 683 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 684 | CLANG_WARN_EMPTY_BODY = YES; 685 | CLANG_WARN_ENUM_CONVERSION = YES; 686 | CLANG_WARN_INFINITE_RECURSION = YES; 687 | CLANG_WARN_INT_CONVERSION = YES; 688 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 689 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 690 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 691 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 692 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 693 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 694 | CLANG_WARN_STRICT_PROTOTYPES = YES; 695 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 696 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 697 | CLANG_WARN_UNREACHABLE_CODE = YES; 698 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 699 | COPY_PHASE_STRIP = NO; 700 | DEBUG_INFORMATION_FORMAT = dwarf; 701 | ENABLE_STRICT_OBJC_MSGSEND = YES; 702 | ENABLE_TESTABILITY = YES; 703 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 704 | GCC_C_LANGUAGE_STANDARD = gnu17; 705 | GCC_DYNAMIC_NO_PIC = NO; 706 | GCC_NO_COMMON_BLOCKS = YES; 707 | GCC_OPTIMIZATION_LEVEL = 0; 708 | GCC_PREPROCESSOR_DEFINITIONS = ( 709 | "DEBUG=1", 710 | "$(inherited)", 711 | ); 712 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 713 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 714 | GCC_WARN_UNDECLARED_SELECTOR = YES; 715 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 716 | GCC_WARN_UNUSED_FUNCTION = YES; 717 | GCC_WARN_UNUSED_VARIABLE = YES; 718 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 719 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 720 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 721 | MTL_FAST_MATH = YES; 722 | ONLY_ACTIVE_ARCH = YES; 723 | SDKROOT = iphoneos; 724 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 725 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 726 | }; 727 | name = Debug; 728 | }; 729 | 6F4ADD052BB52392001A8A13 /* Release */ = { 730 | isa = XCBuildConfiguration; 731 | buildSettings = { 732 | ALWAYS_SEARCH_USER_PATHS = NO; 733 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 734 | CLANG_ANALYZER_NONNULL = YES; 735 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 736 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 737 | CLANG_ENABLE_MODULES = YES; 738 | CLANG_ENABLE_OBJC_ARC = YES; 739 | CLANG_ENABLE_OBJC_WEAK = YES; 740 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 741 | CLANG_WARN_BOOL_CONVERSION = YES; 742 | CLANG_WARN_COMMA = YES; 743 | CLANG_WARN_CONSTANT_CONVERSION = YES; 744 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 745 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 746 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 747 | CLANG_WARN_EMPTY_BODY = YES; 748 | CLANG_WARN_ENUM_CONVERSION = YES; 749 | CLANG_WARN_INFINITE_RECURSION = YES; 750 | CLANG_WARN_INT_CONVERSION = YES; 751 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 752 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 753 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 754 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 755 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 756 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 757 | CLANG_WARN_STRICT_PROTOTYPES = YES; 758 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 759 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 760 | CLANG_WARN_UNREACHABLE_CODE = YES; 761 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 762 | COPY_PHASE_STRIP = NO; 763 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 764 | ENABLE_NS_ASSERTIONS = NO; 765 | ENABLE_STRICT_OBJC_MSGSEND = YES; 766 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 767 | GCC_C_LANGUAGE_STANDARD = gnu17; 768 | GCC_NO_COMMON_BLOCKS = YES; 769 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 770 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 771 | GCC_WARN_UNDECLARED_SELECTOR = YES; 772 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 773 | GCC_WARN_UNUSED_FUNCTION = YES; 774 | GCC_WARN_UNUSED_VARIABLE = YES; 775 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 776 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 777 | MTL_ENABLE_DEBUG_INFO = NO; 778 | MTL_FAST_MATH = YES; 779 | SDKROOT = iphoneos; 780 | SWIFT_COMPILATION_MODE = wholemodule; 781 | VALIDATE_PRODUCT = YES; 782 | }; 783 | name = Release; 784 | }; 785 | 6F4ADD072BB52392001A8A13 /* Debug */ = { 786 | isa = XCBuildConfiguration; 787 | buildSettings = { 788 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 789 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 790 | CODE_SIGN_STYLE = Automatic; 791 | CURRENT_PROJECT_VERSION = 1; 792 | DEVELOPMENT_ASSET_PATHS = "\"Redux.Demo.SwiftUI/Preview Content\""; 793 | DEVELOPMENT_TEAM = HF4XEL6XEU; 794 | ENABLE_PREVIEWS = YES; 795 | GENERATE_INFOPLIST_FILE = YES; 796 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 797 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 798 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 799 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 800 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 801 | LD_RUNPATH_SEARCH_PATHS = ( 802 | "$(inherited)", 803 | "@executable_path/Frameworks", 804 | ); 805 | MARKETING_VERSION = 1.0; 806 | PRODUCT_BUNDLE_IDENTIFIER = "com.jason.rapai.Redux-Demo-SwiftUI"; 807 | PRODUCT_NAME = "$(TARGET_NAME)"; 808 | SWIFT_EMIT_LOC_STRINGS = YES; 809 | SWIFT_VERSION = 5.0; 810 | TARGETED_DEVICE_FAMILY = "1,2"; 811 | }; 812 | name = Debug; 813 | }; 814 | 6F4ADD082BB52392001A8A13 /* Release */ = { 815 | isa = XCBuildConfiguration; 816 | buildSettings = { 817 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 818 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 819 | CODE_SIGN_STYLE = Automatic; 820 | CURRENT_PROJECT_VERSION = 1; 821 | DEVELOPMENT_ASSET_PATHS = "\"Redux.Demo.SwiftUI/Preview Content\""; 822 | DEVELOPMENT_TEAM = HF4XEL6XEU; 823 | ENABLE_PREVIEWS = YES; 824 | GENERATE_INFOPLIST_FILE = YES; 825 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 826 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 827 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 828 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 829 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 830 | LD_RUNPATH_SEARCH_PATHS = ( 831 | "$(inherited)", 832 | "@executable_path/Frameworks", 833 | ); 834 | MARKETING_VERSION = 1.0; 835 | PRODUCT_BUNDLE_IDENTIFIER = "com.jason.rapai.Redux-Demo-SwiftUI"; 836 | PRODUCT_NAME = "$(TARGET_NAME)"; 837 | SWIFT_EMIT_LOC_STRINGS = YES; 838 | SWIFT_VERSION = 5.0; 839 | TARGETED_DEVICE_FAMILY = "1,2"; 840 | }; 841 | name = Release; 842 | }; 843 | 6F4ADD0A2BB52392001A8A13 /* Debug */ = { 844 | isa = XCBuildConfiguration; 845 | buildSettings = { 846 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 847 | BUNDLE_LOADER = "$(TEST_HOST)"; 848 | CODE_SIGN_STYLE = Automatic; 849 | CURRENT_PROJECT_VERSION = 1; 850 | DEVELOPMENT_TEAM = HF4XEL6XEU; 851 | GENERATE_INFOPLIST_FILE = YES; 852 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 853 | MARKETING_VERSION = 1.0; 854 | PRODUCT_BUNDLE_IDENTIFIER = "com.jason.rapai.Redux-Demo-SwiftUITests"; 855 | PRODUCT_NAME = "$(TARGET_NAME)"; 856 | SWIFT_EMIT_LOC_STRINGS = NO; 857 | SWIFT_VERSION = 5.0; 858 | TARGETED_DEVICE_FAMILY = "1,2"; 859 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Redux.Demo.SwiftUI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Redux.Demo.SwiftUI"; 860 | }; 861 | name = Debug; 862 | }; 863 | 6F4ADD0B2BB52392001A8A13 /* Release */ = { 864 | isa = XCBuildConfiguration; 865 | buildSettings = { 866 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 867 | BUNDLE_LOADER = "$(TEST_HOST)"; 868 | CODE_SIGN_STYLE = Automatic; 869 | CURRENT_PROJECT_VERSION = 1; 870 | DEVELOPMENT_TEAM = HF4XEL6XEU; 871 | GENERATE_INFOPLIST_FILE = YES; 872 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 873 | MARKETING_VERSION = 1.0; 874 | PRODUCT_BUNDLE_IDENTIFIER = "com.jason.rapai.Redux-Demo-SwiftUITests"; 875 | PRODUCT_NAME = "$(TARGET_NAME)"; 876 | SWIFT_EMIT_LOC_STRINGS = NO; 877 | SWIFT_VERSION = 5.0; 878 | TARGETED_DEVICE_FAMILY = "1,2"; 879 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Redux.Demo.SwiftUI.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Redux.Demo.SwiftUI"; 880 | }; 881 | name = Release; 882 | }; 883 | 6F4ADD0D2BB52392001A8A13 /* Debug */ = { 884 | isa = XCBuildConfiguration; 885 | buildSettings = { 886 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 887 | CODE_SIGN_STYLE = Automatic; 888 | CURRENT_PROJECT_VERSION = 1; 889 | DEVELOPMENT_TEAM = HF4XEL6XEU; 890 | GENERATE_INFOPLIST_FILE = YES; 891 | MARKETING_VERSION = 1.0; 892 | PRODUCT_BUNDLE_IDENTIFIER = "com.jason.rapai.Redux-Demo-SwiftUIUITests"; 893 | PRODUCT_NAME = "$(TARGET_NAME)"; 894 | SWIFT_EMIT_LOC_STRINGS = NO; 895 | SWIFT_VERSION = 5.0; 896 | TARGETED_DEVICE_FAMILY = "1,2"; 897 | TEST_TARGET_NAME = Redux.Demo.SwiftUI; 898 | }; 899 | name = Debug; 900 | }; 901 | 6F4ADD0E2BB52392001A8A13 /* Release */ = { 902 | isa = XCBuildConfiguration; 903 | buildSettings = { 904 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 905 | CODE_SIGN_STYLE = Automatic; 906 | CURRENT_PROJECT_VERSION = 1; 907 | DEVELOPMENT_TEAM = HF4XEL6XEU; 908 | GENERATE_INFOPLIST_FILE = YES; 909 | MARKETING_VERSION = 1.0; 910 | PRODUCT_BUNDLE_IDENTIFIER = "com.jason.rapai.Redux-Demo-SwiftUIUITests"; 911 | PRODUCT_NAME = "$(TARGET_NAME)"; 912 | SWIFT_EMIT_LOC_STRINGS = NO; 913 | SWIFT_VERSION = 5.0; 914 | TARGETED_DEVICE_FAMILY = "1,2"; 915 | TEST_TARGET_NAME = Redux.Demo.SwiftUI; 916 | }; 917 | name = Release; 918 | }; 919 | /* End XCBuildConfiguration section */ 920 | 921 | /* Begin XCConfigurationList section */ 922 | 6F4ADCDD2BB52391001A8A13 /* Build configuration list for PBXProject "Redux.Demo.SwiftUI" */ = { 923 | isa = XCConfigurationList; 924 | buildConfigurations = ( 925 | 6F4ADD042BB52392001A8A13 /* Debug */, 926 | 6F4ADD052BB52392001A8A13 /* Release */, 927 | ); 928 | defaultConfigurationIsVisible = 0; 929 | defaultConfigurationName = Release; 930 | }; 931 | 6F4ADD062BB52392001A8A13 /* Build configuration list for PBXNativeTarget "Redux.Demo.SwiftUI" */ = { 932 | isa = XCConfigurationList; 933 | buildConfigurations = ( 934 | 6F4ADD072BB52392001A8A13 /* Debug */, 935 | 6F4ADD082BB52392001A8A13 /* Release */, 936 | ); 937 | defaultConfigurationIsVisible = 0; 938 | defaultConfigurationName = Release; 939 | }; 940 | 6F4ADD092BB52392001A8A13 /* Build configuration list for PBXNativeTarget "Redux.Demo.SwiftUITests" */ = { 941 | isa = XCConfigurationList; 942 | buildConfigurations = ( 943 | 6F4ADD0A2BB52392001A8A13 /* Debug */, 944 | 6F4ADD0B2BB52392001A8A13 /* Release */, 945 | ); 946 | defaultConfigurationIsVisible = 0; 947 | defaultConfigurationName = Release; 948 | }; 949 | 6F4ADD0C2BB52392001A8A13 /* Build configuration list for PBXNativeTarget "Redux.Demo.SwiftUIUITests" */ = { 950 | isa = XCConfigurationList; 951 | buildConfigurations = ( 952 | 6F4ADD0D2BB52392001A8A13 /* Debug */, 953 | 6F4ADD0E2BB52392001A8A13 /* Release */, 954 | ); 955 | defaultConfigurationIsVisible = 0; 956 | defaultConfigurationName = Release; 957 | }; 958 | /* End XCConfigurationList section */ 959 | 960 | /* Begin XCRemoteSwiftPackageReference section */ 961 | 6F8F6F1B2BB526B900658766 /* XCRemoteSwiftPackageReference "CombineExt" */ = { 962 | isa = XCRemoteSwiftPackageReference; 963 | repositoryURL = "https://github.com/CombineCommunity/CombineExt"; 964 | requirement = { 965 | kind = exactVersion; 966 | version = 1.8.1; 967 | }; 968 | }; 969 | 6FB217B22BBFB776002FBA7F /* XCRemoteSwiftPackageReference "BusyIndicator" */ = { 970 | isa = XCRemoteSwiftPackageReference; 971 | repositoryURL = "git@github.com:jasonjrr/BusyIndicator.git"; 972 | requirement = { 973 | kind = exactVersion; 974 | version = 1.0.0; 975 | }; 976 | }; 977 | /* End XCRemoteSwiftPackageReference section */ 978 | 979 | /* Begin XCSwiftPackageProductDependency section */ 980 | 6F8F6F1C2BB526B900658766 /* CombineExt */ = { 981 | isa = XCSwiftPackageProductDependency; 982 | package = 6F8F6F1B2BB526B900658766 /* XCRemoteSwiftPackageReference "CombineExt" */; 983 | productName = CombineExt; 984 | }; 985 | 6FB217B32BBFB776002FBA7F /* BusyIndicator */ = { 986 | isa = XCSwiftPackageProductDependency; 987 | package = 6FB217B22BBFB776002FBA7F /* XCRemoteSwiftPackageReference "BusyIndicator" */; 988 | productName = BusyIndicator; 989 | }; 990 | /* End XCSwiftPackageProductDependency section */ 991 | }; 992 | rootObject = 6F4ADCDA2BB52391001A8A13 /* Project object */; 993 | } 994 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "dd184b619a558be485ae26a3ac0e16f112a4c445068d6a0bb37afae00b5482cd", 3 | "pins" : [ 4 | { 5 | "identity" : "busyindicator", 6 | "kind" : "remoteSourceControl", 7 | "location" : "git@github.com:jasonjrr/BusyIndicator.git", 8 | "state" : { 9 | "revision" : "dd4d5e4160372accad6ff54beb69c3354c85f63b", 10 | "version" : "1.0.0" 11 | } 12 | }, 13 | { 14 | "identity" : "combineext", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/CombineCommunity/CombineExt", 17 | "state" : { 18 | "revision" : "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", 19 | "version" : "1.8.1" 20 | } 21 | } 22 | ], 23 | "version" : 3 24 | } 25 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI.xcodeproj/xcshareddata/xcschemes/Redux.Demo.SwiftUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 9 | 10 | 16 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 35 | 36 | 37 | 38 | 40 | 46 | 47 | 48 | 51 | 57 | 58 | 59 | 60 | 61 | 71 | 73 | 79 | 80 | 81 | 82 | 88 | 90 | 96 | 97 | 98 | 99 | 101 | 102 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI.xctestplan: -------------------------------------------------------------------------------- 1 | { 2 | "configurations" : [ 3 | { 4 | "id" : "B9F60465-4EBF-4EA1-AD5C-E505F3726DFC", 5 | "name" : "Test Scheme Action", 6 | "options" : { 7 | 8 | } 9 | } 10 | ], 11 | "defaultOptions" : { 12 | "targetForVariableExpansion" : { 13 | "containerPath" : "container:Redux.Demo.SwiftUI.xcodeproj", 14 | "identifier" : "6F4ADCE12BB52391001A8A13", 15 | "name" : "Redux.Demo.SwiftUI" 16 | } 17 | }, 18 | "testTargets" : [ 19 | { 20 | "target" : { 21 | "containerPath" : "container:Redux.Demo.SwiftUI.xcodeproj", 22 | "identifier" : "6F4ADCF12BB52392001A8A13", 23 | "name" : "Redux.Demo.SwiftUITests" 24 | } 25 | }, 26 | { 27 | "enabled" : false, 28 | "parallelizable" : true, 29 | "target" : { 30 | "containerPath" : "container:Redux.Demo.SwiftUI.xcodeproj", 31 | "identifier" : "6F4ADCFB2BB52392001A8A13", 32 | "name" : "Redux.Demo.SwiftUIUITests" 33 | } 34 | } 35 | ], 36 | "version" : 1 37 | } 38 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Extensions/Combine/Publishers+Async.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publishers+Async.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/26/24. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | public enum AsyncError: LocalizedError { 12 | case finishedWithoutValue 13 | } 14 | 15 | extension AnyPublisher { 16 | public func async() async throws -> Output { 17 | try await withCheckedThrowingContinuation { continuation in 18 | var cancellable: AnyCancellable? 19 | var finishedWithoutValue = true 20 | cancellable = first() 21 | .sink { result in 22 | switch result { 23 | case .finished: 24 | if finishedWithoutValue { 25 | continuation.resume(throwing: AsyncError.finishedWithoutValue) 26 | } 27 | case let .failure(error): 28 | continuation.resume(throwing: error) 29 | } 30 | cancellable?.cancel() 31 | } receiveValue: { value in 32 | finishedWithoutValue = false 33 | continuation.resume(with: .success(value)) 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/5/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | @inlinable static var empty: String { "" } 12 | } 13 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Extensions/SwiftUI/Color+SystemColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+SystemColors.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | extension Color { 12 | static var label: Color { 13 | Color(UIColor.label) 14 | } 15 | 16 | static var secondaryLabel: Color { 17 | Color(UIColor.secondaryLabel) 18 | } 19 | 20 | static var tertiaryLabel: Color { 21 | Color(UIColor.tertiaryLabel) 22 | } 23 | 24 | static var quaternaryLabel: Color { 25 | Color(UIColor.quaternaryLabel) 26 | } 27 | 28 | static var systemFill: Color { 29 | Color(UIColor.systemFill) 30 | } 31 | 32 | static var secondarySystemFill: Color { 33 | Color(UIColor.secondarySystemFill) 34 | } 35 | 36 | static var tertiarySystemFill: Color { 37 | Color(UIColor.tertiarySystemFill) 38 | } 39 | 40 | static var quaternarySystemFill: Color { 41 | Color(UIColor.quaternarySystemFill) 42 | } 43 | 44 | static var systemBackground: Color { 45 | Color(UIColor.systemBackground) 46 | } 47 | 48 | static var secondarySystemBackground: Color { 49 | Color(UIColor.secondarySystemBackground) 50 | } 51 | 52 | static var tertiarySystemBackground: Color { 53 | Color(UIColor.tertiarySystemBackground) 54 | } 55 | 56 | static var systemGroupedBackground: Color { 57 | Color(UIColor.systemGroupedBackground) 58 | } 59 | 60 | static var secondarySystemGroupedBackground: Color { 61 | Color(UIColor.secondarySystemGroupedBackground) 62 | } 63 | 64 | static var tertiarySystemGroupedBackground: Color { 65 | Color(UIColor.tertiarySystemGroupedBackground) 66 | } 67 | 68 | static var systemRed: Color { 69 | Color(UIColor.systemRed) 70 | } 71 | 72 | static var systemBlue: Color { 73 | Color(UIColor.systemBlue) 74 | } 75 | 76 | static var systemPink: Color { 77 | Color(UIColor.systemPink) 78 | } 79 | 80 | static var systemTeal: Color { 81 | Color(UIColor.systemTeal) 82 | } 83 | 84 | static var systemGreen: Color { 85 | Color(UIColor.systemGreen) 86 | } 87 | 88 | static var systemIndigo: Color { 89 | Color(UIColor.systemIndigo) 90 | } 91 | 92 | static var systemOrange: Color { 93 | Color(UIColor.systemOrange) 94 | } 95 | 96 | static var systemPurple: Color { 97 | Color(UIColor.systemPurple) 98 | } 99 | 100 | static var systemYellow: Color { 101 | Color(UIColor.systemYellow) 102 | } 103 | 104 | static var systemGray: Color { 105 | Color(UIColor.systemGray) 106 | } 107 | 108 | static var systemGray2: Color { 109 | Color(UIColor.systemGray2) 110 | } 111 | 112 | static var systemGray3: Color { 113 | Color(UIColor.systemGray3) 114 | } 115 | 116 | static var systemGray4: Color { 117 | Color(UIColor.systemGray4) 118 | } 119 | 120 | static var systemGray5: Color { 121 | Color(UIColor.systemGray5) 122 | } 123 | 124 | static var systemGray6: Color { 125 | Color(UIColor.systemGray6) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Extensions/SwiftUI/EdgeInsets+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsets+Extensions.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension EdgeInsets { 11 | init(all: CGFloat) { 12 | self.init(top: all, leading: all, bottom: all, trailing: all) 13 | } 14 | 15 | init(horizontal: CGFloat, vertical: CGFloat) { 16 | self.init(top: vertical, leading: horizontal, bottom: vertical, trailing: horizontal) 17 | } 18 | 19 | init(top: CGFloat, and remaining: CGFloat) { 20 | self.init(top: top, leading: remaining, bottom: remaining, trailing: remaining) 21 | } 22 | 23 | static func all(_ value: CGFloat) -> EdgeInsets { 24 | EdgeInsets(all: value) 25 | } 26 | 27 | static func horizontal(_ horizontal: CGFloat, vertical: CGFloat) -> EdgeInsets { 28 | EdgeInsets(horizontal: horizontal, vertical: vertical) 29 | } 30 | 31 | static func top(_ top: CGFloat, and remaining: CGFloat) -> EdgeInsets { 32 | EdgeInsets(top: top, and: remaining) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Middleware/UIMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIMiddleware.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/3/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct UIMiddleware: Redux.Middleware { 11 | private let colorService: ColorService 12 | 13 | init(colorService: ColorService) { 14 | self.colorService = colorService 15 | } 16 | 17 | func callAsFunction(action: any Redux.Action, dispatcher: Redux.AnyStoreDispatcher) async -> Redux.MiddlewareResult { 18 | guard let newAction = action as? Actions else { 19 | return .action(action) 20 | } 21 | switch newAction { 22 | case .uiAction(let uiAction): 23 | return await handle(action: action, uiAction: uiAction, dispatcher: dispatcher) 24 | default: 25 | return .action(action) 26 | } 27 | } 28 | 29 | private func handle(action: some Redux.Action, uiAction: Actions.UIActions, dispatcher: Redux.AnyStoreDispatcher) async -> Redux.MiddlewareResult { 30 | switch uiAction { 31 | case .general: return .action(action) 32 | case .colorWizard(let colorWizardAction): 33 | return await handle(action: action, colorWizardAction: colorWizardAction, dispatcher: dispatcher) 34 | case .landingScreen(let landingScreenAction): 35 | switch landingScreenAction { 36 | case .onSignInTapped, .onPulseTapped, .dismissSignInModal, .dismissPulseView, .setColorWizardState: 37 | return .action(action) 38 | case .onSignOutTapped: 39 | await dispatcher.dispatch(action: .user(.signOut)) 40 | return .handled 41 | case .onStartColorWizard: 42 | let colorWizardState = await self.colorService.fetchColorWizardState() 43 | await dispatcher.dispatch(action: Actions.uiAction(.landingScreen(.setColorWizardState(colorWizardState)))) 44 | return .handled 45 | } 46 | } 47 | } 48 | } 49 | 50 | extension UIMiddleware { 51 | private func handle( 52 | action: some Redux.Action, 53 | colorWizardAction: Actions.UIActions.ColorWizardActions, 54 | dispatcher: Redux.AnyStoreDispatcher 55 | ) async -> Redux.MiddlewareResult { 56 | switch colorWizardAction { 57 | case .onBack, .onNext: 58 | return .action(action) 59 | case .onFinish: 60 | await dispatcher.dispatch(action: Actions.uiAction(.landingScreen(.setColorWizardState(nil)))) 61 | return .handled 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Middleware/UserAuthMiddleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserAuthMiddleware.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/29/24. 6 | // 7 | 8 | import Foundation 9 | import BusyIndicator 10 | 11 | class UserAuthMiddleware: Redux.Middleware { 12 | enum Errors: Error { 13 | case failedToSignIn(username: String, password: String) 14 | } 15 | 16 | private let busyIndicatorService: BusyIndicatorServiceProtocol 17 | 18 | init(busyIndicatorService: any BusyIndicatorServiceProtocol) { 19 | self.busyIndicatorService = busyIndicatorService 20 | } 21 | 22 | func callAsFunction(action: any Redux.Action, dispatcher: Redux.AnyStoreDispatcher) async -> Redux.MiddlewareResult { 23 | guard let newAction = action as? Actions else { 24 | return .action(action) 25 | } 26 | switch newAction { 27 | case .userAction(let userAction): 28 | return await handle(action: action, userAction: userAction, dispatcher: dispatcher) 29 | default: 30 | return .action(action) 31 | } 32 | } 33 | 34 | private func handle(action: some Redux.Action, userAction: Actions.UserActions, dispatcher: Redux.AnyStoreDispatcher) async -> Redux.MiddlewareResult { 35 | switch userAction { 36 | case .signIn(let username, let password): 37 | if username.isEmpty || password.isEmpty { 38 | return .action( 39 | .user(.setFailedToSignIn( 40 | error: Errors.failedToSignIn(username: username, password: password), 41 | username: username, 42 | password: password))) 43 | } 44 | await dispatcher.dispatch(action: .user(.signInSuccessful(username: username))) 45 | await dispatcher.dispatch(action: .ui(.landingScreen(.dismissSignInModal))) 46 | return .handled 47 | 48 | case .setFailedToSignIn(let error, let username, _): 49 | await dispatcher.dispatch(action: .ui(.landingScreen(.dismissSignInModal))) 50 | await dispatcher.dispatch(action: .ui(.general(.presentFailedToSignInAlert(error: error, username: username)))) 51 | return .handled 52 | 53 | case .signInSuccessful: break 54 | case .signOut: 55 | let busySubject = self.busyIndicatorService.enqueue() 56 | try? await Task.sleep(for: .seconds(2)) 57 | busySubject.dequeue() 58 | } 59 | return .action(action) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Reducers+Actions/AppReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppReducer.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | /// Note: We only mark the root of actions as a `Redux.Action` to avoid sending a partial action. 11 | enum Actions: Redux.Action { 12 | case uiAction(Actions.UIActions) 13 | case userAction(Actions.UserActions) 14 | 15 | static func == (lhs: Actions, rhs: Actions) -> Bool { 16 | switch (lhs, rhs) { 17 | case (.uiAction(let action1), .uiAction(let action2)): 18 | return action1 == action2 19 | case (.userAction(let action1), .userAction(let action2)): 20 | return action1 == action2 21 | default: 22 | return false 23 | } 24 | } 25 | } 26 | 27 | extension Redux.Action where Self == Actions { 28 | static func ui(_ action: Actions.UIActions) -> any Redux.Action { 29 | Actions.uiAction(action) 30 | } 31 | static func user(_ action: Actions.UserActions) -> any Redux.Action { 32 | Actions.userAction(action) 33 | } 34 | } 35 | 36 | struct AppState: Redux.State { 37 | var ui: UIState = UIState() 38 | var user: UserState = UserState() 39 | 40 | init() {} 41 | } 42 | 43 | enum AppReducer { 44 | static func reduce(_ state: AppState, action: Actions) -> AppState { 45 | var newState = state 46 | switch action { 47 | case .uiAction(let uiAction): 48 | newState.ui = UIReducer.reduce(newState.ui, action: uiAction) 49 | case .userAction(let userAction): 50 | newState.user = UserReducer.reduce(newState.user, action: userAction) 51 | } 52 | return newState 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Reducers+Actions/UIReducer+ColorWizard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIReducer+ColorWizard.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/12/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Actions.UIActions { 11 | enum ColorWizardActions: Equatable { 12 | case onBack 13 | case onNext 14 | case onFinish 15 | 16 | static func == (lhs: ColorWizardActions, rhs: ColorWizardActions) -> Bool { 17 | switch (lhs, rhs) { 18 | case (.onBack, .onBack), (.onNext, .onNext), (.onFinish, .onFinish): 19 | return true 20 | default: 21 | return false 22 | } 23 | } 24 | } 25 | } 26 | 27 | extension UIState { 28 | struct ColorWizardState: Identifiable, Hashable { 29 | var id: UUID = UUID() 30 | 31 | var screens: [ColorWizardScreenState] 32 | var currentScreenIndex: Int 33 | 34 | var canMoveBack: Bool 35 | var canMoveNext: Bool 36 | var canFinish: Bool 37 | 38 | func hash(into hasher: inout Hasher) { 39 | hasher.combine(screens) 40 | hasher.combine(self.currentScreenIndex) 41 | } 42 | 43 | static func ==(lhs: ColorWizardState, rhs: ColorWizardState) -> Bool { 44 | if lhs.screens.count == rhs.screens.count { 45 | for i in 0.. Bool { 100 | if lhs.title != rhs.title { 101 | return false 102 | } 103 | switch (lhs.data, rhs.data) { 104 | case (.color(let lhsColorModel), .color(let rhsColorModel)): 105 | if lhsColorModel != rhsColorModel { 106 | return false 107 | } 108 | case (.summary(let lhsColorModels), .summary(let rhsColorModels)): 109 | if lhsColorModels != rhsColorModels { 110 | return false 111 | } 112 | default: 113 | return false 114 | } 115 | switch (lhs.next, rhs.next) { 116 | case (.none, .none): break 117 | case (.push, .push): break 118 | default: 119 | return false 120 | } 121 | return true 122 | } 123 | } 124 | } 125 | 126 | extension UIReducer { 127 | enum ColorWizardReducer { 128 | static func reduce(_ state: UIState.ColorWizardState?, action: Actions.UIActions.ColorWizardActions) -> UIState.ColorWizardState? { 129 | guard var state else { 130 | return nil 131 | } 132 | switch action { 133 | case .onBack: 134 | guard state.currentScreenIndex > 0 else { 135 | return state 136 | } 137 | state.currentScreenIndex = state.currentScreenIndex - 1 138 | state.screens[state.currentScreenIndex].next = .none 139 | state.canMoveBack = state.currentScreenIndex > 0 140 | state.canMoveNext = state.currentScreenIndex + 1 < state.screens.count 141 | state.canFinish = state.currentScreenIndex + 1 == state.screens.count 142 | return state 143 | 144 | case .onNext: 145 | guard state.currentScreenIndex + 1 < state.screens.count else { 146 | return state 147 | } 148 | state.screens[state.currentScreenIndex].next = .push 149 | state.currentScreenIndex = state.currentScreenIndex + 1 150 | state.canMoveBack = true 151 | state.canMoveNext = state.currentScreenIndex + 1 < state.screens.count 152 | state.canFinish = state.currentScreenIndex + 1 == state.screens.count 153 | return state 154 | 155 | case .onFinish: 156 | fatalError("Handled in middleware") 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Reducers+Actions/UIReducer+General.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIReducer+General.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/12/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Actions.UIActions { 11 | enum GeneralActions: Equatable { 12 | case presentFailedToSignInAlert(error: Error, username: String) 13 | 14 | static func == (lhs: GeneralActions, rhs: GeneralActions) -> Bool { 15 | switch (lhs, rhs) { 16 | case (.presentFailedToSignInAlert(let error1, let username1), .presentFailedToSignInAlert(let error2, let username2)): 17 | return error1.localizedDescription == error2.localizedDescription && username1 == username2 18 | default: 19 | return false 20 | } 21 | } 22 | } 23 | } 24 | 25 | extension UIState { 26 | struct GeneralState: Redux.State { 27 | var failedToSignInAlert: FailedToSignInAlert? 28 | } 29 | 30 | struct FailedToSignInAlert { 31 | let error: Error 32 | let username: String 33 | } 34 | } 35 | 36 | extension UIReducer { 37 | enum GeneralReducer { 38 | static func reduce(_ state: UIState.GeneralState, action: Actions.UIActions.GeneralActions) -> UIState.GeneralState { 39 | switch action { 40 | case .presentFailedToSignInAlert(let error, let username): 41 | var newState = state 42 | newState.failedToSignInAlert = UIState.FailedToSignInAlert(error: error, username: username) 43 | return newState 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Reducers+Actions/UIReducer+LandingScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIReducer+LandingScreen.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/12/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Actions.UIActions { 11 | enum LandingScreenActions: Equatable { 12 | case onSignInTapped 13 | case onSignOutTapped 14 | case onPulseTapped 15 | case onStartColorWizard 16 | case dismissSignInModal 17 | case dismissPulseView 18 | 19 | case setColorWizardState(UIState.ColorWizardState?) 20 | 21 | static func == (lhs: LandingScreenActions, rhs: LandingScreenActions) -> Bool { 22 | switch (lhs, rhs) { 23 | case (.onSignInTapped, .onSignInTapped), 24 | (.onSignOutTapped, .onSignOutTapped), 25 | (.onPulseTapped, .onPulseTapped), 26 | (.onStartColorWizard, .onStartColorWizard), 27 | (.dismissSignInModal, .dismissSignInModal), 28 | (.dismissPulseView, .dismissPulseView): 29 | return true 30 | case (.setColorWizardState(let colorWizardState1), .setColorWizardState(let colorWizardState2)): 31 | return colorWizardState1 == colorWizardState2 32 | default: 33 | return false 34 | } 35 | } 36 | } 37 | } 38 | 39 | extension UIState { 40 | struct LandingScreenState: Redux.State { 41 | var showSignInModal: Bool = false 42 | var showPulseScreen: Bool = false 43 | var colorWizard: ColorWizardState? 44 | } 45 | } 46 | 47 | extension UIReducer { 48 | enum LandingScreenReducer { 49 | static func reduce(_ state: UIState.LandingScreenState, action: Actions.UIActions.LandingScreenActions) -> UIState.LandingScreenState { 50 | switch action { 51 | case .onSignInTapped: 52 | var newState = state 53 | newState.showSignInModal = true 54 | return newState 55 | case .onSignOutTapped: 56 | fatalError("Handled in UIMiddleware") 57 | case .onPulseTapped: 58 | var newState = state 59 | newState.showPulseScreen = true 60 | return newState 61 | case .onStartColorWizard: 62 | fatalError("Handled in UIMiddleware") 63 | case .dismissSignInModal: 64 | var newState = state 65 | newState.showSignInModal = false 66 | return newState 67 | case .dismissPulseView: 68 | var newState = state 69 | newState.showPulseScreen = false 70 | return newState 71 | 72 | case .setColorWizardState(let colorWizardState): 73 | var newState = state 74 | newState.colorWizard = colorWizardState 75 | return newState 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Reducers+Actions/UIReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIReducer.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/29/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Actions { 11 | enum UIActions: Equatable { 12 | case general(GeneralActions) 13 | 14 | case colorWizard(ColorWizardActions) 15 | case landingScreen(LandingScreenActions) 16 | 17 | static func == (lhs: UIActions, rhs: UIActions) -> Bool { 18 | switch (lhs, rhs) { 19 | case (.general(let action1), .general(let action2)): 20 | return action1 == action2 21 | case (.colorWizard(let action1), .colorWizard(let action2)): 22 | return action1 == action2 23 | case (.landingScreen(let action1), .landingScreen(let action2)): 24 | return action1 == action2 25 | default: 26 | return false 27 | } 28 | } 29 | } 30 | } 31 | 32 | struct UIState: Redux.State { 33 | var general: GeneralState = GeneralState() 34 | var landingScreen: LandingScreenState = LandingScreenState() 35 | 36 | init() {} 37 | } 38 | 39 | enum UIReducer { 40 | static func reduce(_ state: UIState, action: Actions.UIActions) -> UIState { 41 | switch action { 42 | case .general(let action): 43 | var newState = state 44 | newState.general = GeneralReducer 45 | .reduce(newState.general, action: action) 46 | return newState 47 | case .landingScreen(let action): 48 | var newState = state 49 | newState.landingScreen = LandingScreenReducer 50 | .reduce(newState.landingScreen, action: action) 51 | return newState 52 | case .colorWizard(let action): 53 | var newState = state 54 | newState.landingScreen.colorWizard = ColorWizardReducer 55 | .reduce(newState.landingScreen.colorWizard, action: action) 56 | return newState 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Reducers+Actions/UserReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserReducer.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Actions { 11 | enum UserActions: Equatable { 12 | case signIn(username: String, password: String) 13 | case setFailedToSignIn(error: Error, username: String, password: String) 14 | case signInSuccessful(username: String) 15 | case signOut 16 | 17 | static func == (lhs: UserActions, rhs: UserActions) -> Bool { 18 | switch (lhs, rhs) { 19 | case (.signIn(let username1, let password1), .signIn(let username2, let password2)): 20 | return username1 == username2 && password1 == password2 21 | case (.setFailedToSignIn(let error1, let username1, let password1), .setFailedToSignIn(let error2, let username2, let password2)): 22 | return error1.localizedDescription == error2.localizedDescription && username1 == username2 && password1 == password2 23 | case (.signInSuccessful(let username1), .signInSuccessful(let username2)): 24 | return username1 == username2 25 | case (.signOut, .signOut): 26 | return true 27 | default: 28 | return false 29 | } 30 | } 31 | } 32 | } 33 | 34 | struct UserState: Redux.State { 35 | var username: String? = nil 36 | var userSignInError: Error? = nil 37 | 38 | init() {} 39 | } 40 | 41 | enum UserReducer { 42 | static func reduce(_ state: UserState, action: Actions.UserActions) -> UserState { 43 | switch action { 44 | case .signIn: return state 45 | case .setFailedToSignIn(let error, _, _): 46 | var newState = state 47 | newState.userSignInError = error 48 | return newState 49 | case .signInSuccessful(let username): 50 | var newState = state 51 | newState.username = username 52 | newState.userSignInError = nil 53 | return newState 54 | 55 | case .signOut: 56 | var newState = state 57 | newState.username = nil 58 | newState.userSignInError = nil 59 | return newState 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Redux/Redux+Middleware.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Redux+Middleware.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/28/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Redux { 11 | public enum MiddlewareResult: Equatable { 12 | case handled 13 | case action(any Redux.Action) 14 | 15 | public static func == (lhs: MiddlewareResult, rhs: MiddlewareResult) -> Bool { 16 | switch (lhs, rhs) { 17 | case (.handled, .handled): 18 | return true 19 | case (.action(let action1), .action(let action2)): 20 | return action1.eraseToAnyAction() == action2.eraseToAnyAction() 21 | default: 22 | return false 23 | } 24 | } 25 | } 26 | 27 | public protocol Middleware { 28 | func callAsFunction(action: any Redux.Action, dispatcher: Redux.AnyStoreDispatcher) async -> Redux.MiddlewareResult 29 | } 30 | 31 | struct EchoMiddleware: Middleware { 32 | public func callAsFunction(action: any Redux.Action, dispatcher: Redux.AnyStoreDispatcher) async -> Redux.MiddlewareResult { 33 | .action(action) 34 | } 35 | } 36 | 37 | public struct MiddlewarePipeline: Middleware { 38 | private let middleware: [any Middleware] 39 | 40 | public init(_ middleware: any Middleware...) { 41 | self.middleware = middleware 42 | } 43 | 44 | public init(_ middleware: [any Middleware]) { 45 | self.middleware = middleware 46 | } 47 | 48 | public func callAsFunction(action: any Redux.Action, dispatcher: Redux.AnyStoreDispatcher) async -> MiddlewareResult { 49 | var currentAction: any Redux.Action = action 50 | for m in middleware { 51 | let result = await m(action: currentAction, dispatcher: dispatcher) 52 | switch result { 53 | case .handled: 54 | return .handled 55 | case .action(let action): 56 | currentAction = action 57 | } 58 | } 59 | return .action(currentAction) 60 | } 61 | } 62 | 63 | @resultBuilder 64 | public struct MiddlewareBuilder { 65 | public static func buildArray( 66 | _ components: [MiddlewarePipeline] 67 | ) -> some Middleware { 68 | MiddlewarePipeline(components) 69 | } 70 | 71 | public static func buildBlock( 72 | _ components: any Middleware... 73 | ) -> MiddlewarePipeline { 74 | MiddlewarePipeline(components) 75 | } 76 | 77 | public static func buildEither( 78 | first component: M 79 | ) -> some Middleware { 80 | component 81 | } 82 | 83 | public static func buildEither( 84 | second component: M 85 | ) -> some Middleware { 86 | component 87 | } 88 | 89 | public static func buildExpression( 90 | _ expression: M 91 | ) -> some Middleware { 92 | expression 93 | } 94 | 95 | public static func buildFinalResult( 96 | _ component: M 97 | ) -> some Middleware { 98 | component 99 | } 100 | 101 | public static func buildOptional( 102 | _ component: MiddlewarePipeline? 103 | ) -> Middleware { 104 | guard let component = component else { 105 | return EchoMiddleware() 106 | } 107 | return component 108 | } 109 | } 110 | } 111 | 112 | extension Redux.Middleware { 113 | public func eraseToAnyMiddleware() -> some Redux.Middleware { 114 | self 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Redux/Redux+State.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Redux+State.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/27/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Redux { 11 | public protocol State { 12 | init() 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Redux/Redux+Store+Enviroment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Redux+Store+Enviroment.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/29/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension View { 11 | @inlinable 12 | public func reduxStore(_ store: Redux.Store) -> some View { 13 | environmentObject(store) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Redux/Redux+Store.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Redux+Store.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/28/24. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | extension Redux { 12 | public actor Store: ObservableObject { 13 | public typealias Reducer = (S, A) -> S 14 | 15 | private let _state: CurrentValueSubject 16 | public nonisolated var state: AnyPublisher { self._state.eraseToAnyPublisher() } 17 | 18 | private let middleware: Middleware 19 | private let reducer: Reducer 20 | 21 | public init( 22 | initialState: S = S(), 23 | reducer: @escaping Reducer, 24 | @MiddlewareBuilder middleware: () -> some Middleware 25 | ) { 26 | self._state = CurrentValueSubject(initialState) 27 | self.reducer = reducer 28 | self.middleware = middleware() 29 | } 30 | 31 | public init( 32 | initialState: S = S(), 33 | reducer: @escaping Reducer 34 | ) { 35 | self.init( 36 | initialState: initialState, 37 | reducer: reducer, 38 | middleware: { 39 | EchoMiddleware().eraseToAnyMiddleware() 40 | } 41 | ) 42 | } 43 | 44 | public func dispatch(action: A) async { 45 | let result = await self.middleware(action: action, dispatcher: self.eraseToAnyStoreDispatcher()) 46 | switch result { 47 | case .action(let action): 48 | guard let newAction = action as? A else { 49 | fatalError() 50 | } 51 | let currentState = self._state.value 52 | let newState = self.reducer(currentState, newAction) 53 | self._state.send(newState) 54 | case .handled: 55 | return 56 | } 57 | } 58 | } 59 | } 60 | 61 | extension Redux.Store { 62 | func dispatch(_ factory: () async -> A) async { 63 | await self.dispatch(action: await factory()) 64 | } 65 | 66 | func dispatch( 67 | sequence: Sequence 68 | ) async throws where Sequence.Element == A { 69 | for try await action in sequence { 70 | await dispatch(action: action) 71 | } 72 | } 73 | 74 | func dispatch(future: Future) { 75 | var subscription: AnyCancellable? 76 | subscription = future.sink { _ in 77 | if subscription != nil { 78 | subscription = nil 79 | } 80 | } receiveValue: { action in 81 | guard let action = action else { 82 | return 83 | } 84 | 85 | Task { 86 | await self.dispatch(action: action) 87 | } 88 | } 89 | } 90 | 91 | func dispatch( 92 | publisher: P 93 | ) where P.Output == A, P.Failure == Never { 94 | var subscription: AnyCancellable? 95 | subscription = publisher.sink { _ in 96 | if subscription != nil { 97 | subscription = nil 98 | } 99 | } receiveValue: { action in 100 | Task { 101 | await self.dispatch(action: action) 102 | } 103 | } 104 | } 105 | } 106 | 107 | // MARK: AnyStoreDispatcher 108 | extension Redux { 109 | public actor AnyStoreDispatcher { 110 | private let _dispatch: (any Redux.Action) async -> Void 111 | 112 | init(_dispatch: @escaping (any Redux.Action) async -> Void) { 113 | self._dispatch = _dispatch 114 | } 115 | 116 | public func dispatch(action: any Redux.Action) async { 117 | await self._dispatch(action) 118 | } 119 | 120 | public func dispatchAsync(action: any Redux.Action) { 121 | Task { 122 | await self._dispatch(action) 123 | } 124 | } 125 | } 126 | } 127 | 128 | extension Redux.Store { 129 | public func eraseToAnyStoreDispatcher() -> Redux.AnyStoreDispatcher { 130 | Redux.AnyStoreDispatcher { action async in 131 | guard let action = action as? A else { 132 | fatalError("Unhandled root action \(action)") 133 | } 134 | await self.dispatch(action: action) 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Redux/Redux.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Redux.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/27/24. 6 | // 7 | 8 | import Foundation 9 | 10 | public enum Redux {} 11 | 12 | extension Redux { 13 | public protocol Action: Equatable {} 14 | } 15 | 16 | extension Redux.Action { 17 | public func isEqual(to other: some Redux.Action) -> Bool { 18 | guard let other = other as? Self else { return false } 19 | return self == other 20 | } 21 | 22 | public func eraseToAnyAction() -> Redux.AnyAction { 23 | Redux.AnyAction(wrapped: self) 24 | } 25 | } 26 | 27 | extension Redux { 28 | public struct AnyAction: Redux.Action { 29 | public let wrapped: any Redux.Action 30 | 31 | init(wrapped: some Redux.Action) { 32 | self.wrapped = wrapped 33 | } 34 | 35 | public static func == (lhs: AnyAction, rhs: AnyAction) -> Bool { 36 | lhs.wrapped.isEqual(to: rhs.wrapped) 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Redux_Demo_SwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Redux_Demo_SwiftUIApp.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/27/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct Redux_Demo_SwiftUIApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | AppRootView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/Services/ColorService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorService.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/5/24. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | enum ColorModel: String, CaseIterable, Identifiable { 13 | case blue 14 | case green 15 | case orange 16 | case pink 17 | case purple 18 | case red 19 | case yellow 20 | case white 21 | 22 | var id: String { 23 | self.rawValue 24 | } 25 | } 26 | 27 | extension ColorModel { 28 | func asColor() -> Color { 29 | switch self { 30 | case .blue: return .blue 31 | case .green: return .green 32 | case .orange: return .orange 33 | case .pink: return .pink 34 | case .purple: return .purple 35 | case .red: return .red 36 | case .white: return .white 37 | case .yellow: return .yellow 38 | } 39 | } 40 | } 41 | 42 | protocol ColorServiceProtocol: ObservableObject { 43 | func getNextColor() -> ColorModel 44 | func generateColors(every timeInterval: TimeInterval, on runLoop: RunLoop) -> AnyPublisher 45 | func fetchColorWizardState() async -> UIState.ColorWizardState 46 | } 47 | 48 | extension ColorServiceProtocol { 49 | func generateColors(every timeInterval: TimeInterval = 1.0, on runLoop: RunLoop = .main) -> AnyPublisher { 50 | generateColors(every: timeInterval, on: runLoop) 51 | } 52 | } 53 | 54 | class ColorService: ColorServiceProtocol { 55 | private var index: Int = 0 56 | 57 | func getNextColor() -> ColorModel { 58 | let selection = self.index % 7 59 | self.index = self.index + 1 60 | switch selection { 61 | case 0: return .blue 62 | case 1: return .green 63 | case 2: return .orange 64 | case 3: return .pink 65 | case 4: return .purple 66 | case 5: return .red 67 | case 6: return .yellow 68 | default: return .white 69 | } 70 | } 71 | 72 | func generateColors(every timeInterval: TimeInterval = 1.0, on runLoop: RunLoop = .main) -> AnyPublisher { 73 | return Timer.publish(every: timeInterval, on: runLoop, in: .default) 74 | .autoconnect() 75 | .map { timer in 76 | let selection = Int(timer.timeIntervalSince1970 * 1.5) % 7 77 | switch selection { 78 | case 0: return .blue 79 | case 1: return .green 80 | case 2: return .orange 81 | case 3: return .pink 82 | case 4: return .purple 83 | case 5: return .red 84 | case 6: return .yellow 85 | default: return .white 86 | } 87 | } 88 | .eraseToAnyPublisher() 89 | } 90 | 91 | func fetchColorWizardState() async -> UIState.ColorWizardState { 92 | let config = ColorWizardConfiguration.mock() 93 | let screens = config.pages.compactMap { page in 94 | if let color = page.color { 95 | return UIState.ColorWizardScreenState(title: page.title, data: .color(color)) 96 | } else if let colors = page.colors { 97 | return UIState.ColorWizardScreenState(title: page.title, data: .summary(colors)) 98 | } else { 99 | return nil 100 | } 101 | } 102 | return UIState.ColorWizardState( 103 | screens: screens, currentScreenIndex: 0, 104 | canMoveBack: false, 105 | canMoveNext: screens.count > 1, 106 | canFinish: screens.count == 1) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/AppRootView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppRootView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/27/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppRootView: View { 11 | @State private var path = NavigationPath() 12 | 13 | var body: some View { 14 | AppDependencyContainerView { 15 | NavigationStack(path: self.$path) { 16 | LandingView() 17 | } 18 | } 19 | } 20 | } 21 | 22 | #Preview { 23 | AppRootView() 24 | } 25 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/ColorWizard/ColorWizardColorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardColorView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorWizardColorView: View { 11 | private let color: ColorModel 12 | private let index: Int 13 | 14 | init(color: ColorModel, index: Int) { 15 | self.color = color 16 | self.index = index 17 | } 18 | 19 | var body: some View { 20 | self.color.asColor().ignoresSafeArea() 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/ColorWizard/ColorWizardRoutesView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardRoutesView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorWizardRoutesView: View { 11 | @EnvironmentObject var store: AppReduxStore 12 | 13 | @State private var path = NavigationPath() 14 | 15 | init() {} 16 | 17 | var body: some View { 18 | NavigationStack(path: self.$path) { 19 | DestinationView(state: nil, index: 0) 20 | } 21 | } 22 | } 23 | 24 | extension ColorWizardRoutesView { 25 | fileprivate struct ScreenView: View { 26 | private let state: UIState.ColorWizardScreenState? 27 | private let index: Int 28 | 29 | init(state: UIState.ColorWizardScreenState?, index: Int) { 30 | self.state = state 31 | self.index = index 32 | } 33 | 34 | @ViewBuilder 35 | var body: some View { 36 | if let state { 37 | switch state.data { 38 | case .color(let colorModel): 39 | ColorWizardColorView(color: colorModel, index: self.index) 40 | case .summary(let colorModels): 41 | ColorWizardSummaryView(colors: colorModels, index: self.index) 42 | } 43 | } 44 | } 45 | } 46 | 47 | fileprivate struct DestinationView: View { 48 | @EnvironmentObject private var store: AppReduxStore 49 | 50 | private let index: Int 51 | 52 | @State var state: UIState.ColorWizardScreenState? 53 | @State var canMoveBack: Bool = false 54 | @State var canMoveNext: Bool = false 55 | @State var canFinish: Bool = false 56 | 57 | @State var next: UIState.ColorWizardScreenState? 58 | 59 | init(state: UIState.ColorWizardScreenState?, index: Int) { 60 | self.state = state 61 | self.index = index 62 | } 63 | 64 | var body: some View { 65 | buildScreen(state: self.state, index: self.index) 66 | .onReceive(self.store.state 67 | .map { $0.ui.landingScreen.colorWizard } 68 | .filter { $0 != nil } 69 | .map { $0! } 70 | .receive(on: RunLoop.main) 71 | ) { (colorWizard: UIState.ColorWizardState) in 72 | self.state = colorWizard.screens[self.index] 73 | self.canMoveBack = colorWizard.canMoveBack 74 | self.canMoveNext = colorWizard.canMoveNext 75 | self.canFinish = colorWizard.canFinish 76 | 77 | switch self.state?.next { 78 | case .push: 79 | self.next = colorWizard.screens[self.index + 1] 80 | default: 81 | self.next = nil 82 | } 83 | } 84 | .navigationDestination(item: self.$next) { next in 85 | DestinationView(state: next, index: self.index + 1) 86 | } 87 | } 88 | 89 | @ViewBuilder 90 | private func buildScreen(state: UIState.ColorWizardScreenState?, index: Int) -> some View { 91 | ScreenView(state: state, index: index) 92 | .navigationTitle(state?.title ?? .empty) 93 | .navigationBarBackButtonHidden(true) 94 | .toolbar { 95 | ToolbarItem(placement: .navigationBarLeading) { 96 | if self.canMoveBack { 97 | Button { 98 | Task { 99 | await self.store.dispatch(action: .uiAction(.colorWizard(.onBack))) 100 | } 101 | } label: { 102 | Text("Back") 103 | } 104 | } 105 | } 106 | ToolbarItem(placement: .navigationBarTrailing) { 107 | if self.canMoveNext { 108 | Button { 109 | Task { 110 | await self.store.dispatch(action: .uiAction(.colorWizard(.onNext))) 111 | } 112 | } label: { 113 | Text("Forward") 114 | } 115 | } else if self.canFinish { 116 | Button { 117 | Task { 118 | await self.store.dispatch(action: .uiAction(.colorWizard(.onFinish))) 119 | } 120 | } label: { 121 | Text("Done") 122 | } 123 | } 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | #Preview { 131 | AppDependencyContainerView { store in 132 | await store.dispatch(action: .uiAction(.landingScreen(.onStartColorWizard))) 133 | } content: { 134 | ColorWizardRoutesView() 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/ColorWizard/ColorWizardSummaryView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardSummaryView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/6/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorWizardSummaryView: View { 11 | private let colors: [ColorModel] 12 | private let index: Int 13 | 14 | init(colors: [ColorModel], index: Int) { 15 | self.colors = colors 16 | self.index = index 17 | } 18 | 19 | var body: some View { 20 | ScrollView { 21 | VStack { 22 | ForEach(self.colors) { color in 23 | RoundedRectangle(cornerRadius: 36.0, style: .continuous) 24 | .fill(color.asColor()) 25 | .frame(maxWidth: .infinity, minHeight: 54.0, idealHeight: 54.0, maxHeight: 54.0) 26 | .padding([.leading, .trailing], 16.0) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/ColorWizard/Configuration/ColorWizardConfiguration+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardConfiguration+Mock.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | extension ColorWizardConfiguration { 11 | static func mock() -> ColorWizardConfiguration { 12 | ColorWizardConfiguration(pages: [ 13 | .page("First Color", color: .green), 14 | .page("Second Color", color: .orange), 15 | .page("Third Color", color: .red), 16 | .page("Fourth Color", color: .pink), 17 | .page("Fifth Color", color: .purple), 18 | .page("Summary", colors: [ 19 | .green, 20 | .orange, 21 | .red, 22 | .pink, 23 | .purple, 24 | ]), 25 | ]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/ColorWizard/Configuration/ColorWizardConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardConfiguration.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/6/24. 6 | // 7 | 8 | import Foundation 9 | 10 | struct ColorWizardConfiguration { 11 | let pages: [Page] 12 | } 13 | 14 | extension ColorWizardConfiguration { 15 | struct Page { 16 | let title: String 17 | let color: ColorModel? 18 | let colors: [ColorModel]? 19 | 20 | static func page(_ title: String, color: ColorModel? = nil, colors: [ColorModel]? = nil) -> Page { 21 | Page(title: title, color: color, colors: colors) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/LandingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LandingView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | import BusyIndicator 10 | 11 | struct LandingView: View { 12 | @EnvironmentObject private var store: AppReduxStore 13 | @EnvironmentObject private var colorService: ColorService 14 | 15 | @ScaledMetric private var buttonPadding: CGFloat = 8.0 16 | @ScaledMetric private var inverseHorizontalPadding: CGFloat = 8.0 17 | 18 | @State private var isAuthenticated: Bool = false 19 | @State private var username: String = .empty 20 | @State private var pulseColor: Color = .accentColor 21 | 22 | @State private var showSignIn: Bool = false 23 | @State private var showPulseView: Bool = false 24 | @State private var colorWizard: UIState.ColorWizardState? 25 | 26 | var body: some View { 27 | ZStack { 28 | VStack(alignment: .center, spacing: 24.0) { 29 | signInOutButton() 30 | pulseButton() 31 | colorWizardButton() 32 | } 33 | .padding([.leading, .trailing], max(56.0 - self.inverseHorizontalPadding, 4.0)) 34 | } 35 | .frame(maxWidth: .infinity, maxHeight: .infinity) 36 | .overlay { 37 | if self.showSignIn { 38 | SignInCardView() 39 | } 40 | } 41 | .navigationBarHidden(true) 42 | .onReceive(self.store.state 43 | .map { $0.user.username } 44 | .removeDuplicates() 45 | .receive(on: RunLoop.main) 46 | ) { username in 47 | self.username = username ?? .empty 48 | self.isAuthenticated = username != nil 49 | } 50 | .onReceive(self.store.state 51 | .map { $0.ui.landingScreen.showSignInModal } 52 | .removeDuplicates() 53 | .receive(on: RunLoop.main) 54 | ) { showSignInModal in 55 | self.showSignIn = showSignInModal 56 | } 57 | .onReceive(self.store.state 58 | .map { $0.ui.landingScreen.showPulseScreen } 59 | .removeDuplicates() 60 | .debounce(for: 0.05, scheduler: RunLoop.main) 61 | .receive(on: RunLoop.main) 62 | ) { showPulseView in 63 | self.showPulseView = showPulseView 64 | } 65 | .onReceive(self.store.state 66 | .map { $0.ui.landingScreen.colorWizard } 67 | .removeDuplicates() 68 | .receive(on: RunLoop.main) 69 | ) { 70 | self.colorWizard = $0 71 | } 72 | .onChange(of: self.showPulseView) { 73 | if !self.showPulseView { 74 | Task { 75 | await self.store.dispatch(action: 76 | .uiAction(.landingScreen(.dismissPulseView))) 77 | } 78 | } 79 | } 80 | .onReceive(self.colorService 81 | .generateColors() 82 | .receive(on: RunLoop.main) 83 | ) { colorModel in 84 | withAnimation(.easeInOut) { 85 | self.pulseColor = colorModel.asColor() 86 | } 87 | } 88 | .navigationDestination(isPresented: self.$showPulseView) { 89 | PulseView() 90 | } 91 | .fullScreenCover(item: self.$colorWizard) { _ in 92 | ColorWizardRoutesView() 93 | } 94 | } 95 | 96 | private func signInOutButton() -> some View { 97 | Button { 98 | Task { 99 | if self.isAuthenticated { 100 | await self.store.dispatch(action: 101 | .uiAction(.landingScreen(.onSignOutTapped))) 102 | } else { 103 | await self.store.dispatch(action: 104 | .uiAction(.landingScreen(.onSignInTapped))) 105 | } 106 | } 107 | } label: { 108 | Text(self.isAuthenticated ? "Sign Out, \(self.username)" : "Sign In") 109 | .multilineTextAlignment(.center) 110 | .lineLimit(nil) 111 | .padding(self.buttonPadding) 112 | .frame(maxWidth: .infinity, minHeight: 54.0) 113 | .contentShape(Rectangle()) 114 | } 115 | .buttonStyle(.brightBorderedButton) 116 | .busyOverlay() 117 | .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) 118 | } 119 | 120 | private func pulseButton() -> some View { 121 | Button { 122 | Task { 123 | await self.store.dispatch(action: 124 | .uiAction(.landingScreen(.onPulseTapped))) 125 | } 126 | } label: { 127 | Text("Pulse") 128 | .padding(self.buttonPadding) 129 | .frame(maxWidth: .infinity, minHeight: 54.0) 130 | .contentShape(Rectangle()) 131 | } 132 | .buttonStyle(.brightBorderedButton(color: self.pulseColor)) 133 | } 134 | 135 | private func colorWizardButton() -> some View { 136 | Button { 137 | Task { 138 | await self.store.dispatch(action: .uiAction(.landingScreen(.onStartColorWizard))) 139 | } 140 | } label: { 141 | Text("Color Wizard") 142 | .multilineTextAlignment(.center) 143 | .lineLimit(nil) 144 | .padding(self.buttonPadding) 145 | .frame(maxWidth: .infinity, minHeight: 54.0) 146 | .contentShape(Rectangle()) 147 | } 148 | .buttonStyle(.brightBorderedButton) 149 | .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) 150 | } 151 | } 152 | 153 | #Preview { 154 | AppDependencyContainerView { 155 | NavigationStack { 156 | LandingView() 157 | } 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/PulseView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulseView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/5/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PulseView: View { 11 | @EnvironmentObject var store: AppReduxStore 12 | @EnvironmentObject var colorService: ColorService 13 | 14 | @State private var title: String = .empty 15 | @State private var colorItems: [ColorItem] = [] 16 | 17 | var body: some View { 18 | ZStack { 19 | ForEach(self.colorItems) { item in 20 | PulseCircle(colorItem: item) 21 | .frame( 22 | width: max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height), 23 | height: max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height) 24 | ) 25 | } 26 | } 27 | .navigationTitle(self.title) 28 | .navigationBarTitleDisplayMode(.inline) 29 | .overlay(.thinMaterial) 30 | .onReceive(self.store.state 31 | .map { $0.user.username } 32 | .removeDuplicates() 33 | .receive(on: DispatchQueue.main) 34 | ) { 35 | if let username = $0 { 36 | self.title = "Welcome, \(username)" 37 | } else { 38 | self.title = "Hello, mysterious stranger" 39 | } 40 | } 41 | .onReceive(self.colorService 42 | .generateColors() 43 | .receive(on: RunLoop.main) 44 | ) { colorModel in 45 | var colorItems = self.colorItems 46 | colorItems.insert(ColorItem(model: colorModel), at: 0) 47 | if colorItems.count > 3 { 48 | let _ = self.colorItems.popLast() 49 | } 50 | self.colorItems = colorItems 51 | } 52 | } 53 | } 54 | 55 | extension PulseView { 56 | struct ColorItem: Identifiable { 57 | let id: UUID = UUID() 58 | let model: ColorModel 59 | } 60 | 61 | struct PulseCircle: View { 62 | @State private var item: ColorItem 63 | @State private var circleOpacity: Double = 0.0 64 | 65 | init(colorItem: ColorItem) { 66 | self.item = colorItem 67 | } 68 | 69 | var body: some View { 70 | Circle() 71 | .fill(Color.white) 72 | .colorMultiply(self.item.model.asColor()) 73 | .opacity(self.circleOpacity) 74 | .animation(.easeIn, value: self.self.circleOpacity) 75 | .onAppear { 76 | withAnimation { 77 | self.circleOpacity = 1.0 78 | } 79 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) { 80 | withAnimation { 81 | self.circleOpacity = 0.0 82 | } 83 | } 84 | } 85 | } 86 | } 87 | } 88 | 89 | #Preview { 90 | AppDependencyContainerView { 91 | NavigationStack { 92 | PulseView() 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Screens/SignInCardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInCardView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SignInCardView: View { 11 | @EnvironmentObject var store: AppReduxStore 12 | 13 | @ScaledMetric private var buttonFontSize: CGFloat = 18.0 14 | @ScaledMetric private var inverseCardPadding: CGFloat = 16.0 15 | 16 | @State private var showCard: Bool = false 17 | @State private var signInEnabled: Bool = false 18 | 19 | @State private var username: String = .empty 20 | @State private var password: String = .empty 21 | 22 | @FocusState private var focusState: FocusField? 23 | enum FocusField { 24 | case username 25 | case password 26 | } 27 | 28 | var body: some View { 29 | HStack { 30 | if self.showCard { 31 | CardView(color: Color.systemBackground, cornerRadius: .large) { 32 | VStack { 33 | VStack(alignment: .leading) { 34 | Text("User Name") 35 | .padding(EdgeInsets(horizontal: 8.0, vertical: 0.0)) 36 | TextField("User Name", text: self.$username, prompt: nil) 37 | .focused(self.$focusState, equals: .username) 38 | .padding() 39 | .background(RoundedRectangle(cornerRadius: 8.0).stroke(Color.systemGray)) 40 | .contentShape(Rectangle()) 41 | .onTapGesture { 42 | if self.focusState != .username { 43 | self.focusState = .username 44 | } 45 | } 46 | Text("Password").padding(.top) 47 | .padding(EdgeInsets(horizontal: 8.0, vertical: 0.0)) 48 | SecureField("Password", text: self.$password, prompt: nil) 49 | .focused(self.$focusState, equals: .password) 50 | .padding() 51 | .background(RoundedRectangle(cornerRadius: 8.0).stroke(Color.systemGray)) 52 | .contentShape(Rectangle()) 53 | .onTapGesture { 54 | if self.focusState != .password { 55 | self.focusState = .password 56 | } 57 | } 58 | } 59 | .padding(16.0) 60 | .onSubmit { 61 | switch self.focusState { 62 | case .none: break 63 | case .username: self.focusState = .password 64 | case .password: self.focusState = nil 65 | } 66 | } 67 | 68 | HStack { 69 | Button { 70 | Task { 71 | await self.store.dispatch(action: .uiAction(.landingScreen(.dismissSignInModal))) 72 | } 73 | } label: { 74 | Text("Cancel") 75 | .lineLimit(1) 76 | .minimumScaleFactor(0.75) 77 | .font(.system(size: self.buttonFontSize)) 78 | .bold() 79 | .frame(maxWidth: .infinity, minHeight: 48.0, idealHeight: 48.0, maxHeight: 48.0) 80 | .contentShape(Rectangle()) 81 | } 82 | 83 | Button { 84 | Task { 85 | await self.store.dispatch(action: Actions.userAction( 86 | .signIn(username: self.username, password: self.password))) 87 | } 88 | } label: { 89 | Text("Sign In") 90 | .lineLimit(1) 91 | .minimumScaleFactor(0.75) 92 | .font(.system(size: self.buttonFontSize)) 93 | .frame(maxWidth: .infinity, minHeight: 48.0) 94 | .contentShape(Rectangle()) 95 | } 96 | .disabled(!self.signInEnabled) 97 | } 98 | .padding(.bottom) 99 | } 100 | } 101 | .fixedSize(horizontal: false, vertical: true) 102 | .clipped() 103 | .shadow(radius: 3.0) 104 | .padding(max(52.0 - self.inverseCardPadding, 4.0)) 105 | .transition(.scale(scale: 0.0)) 106 | } 107 | } 108 | .frame(maxWidth: .infinity, maxHeight: .infinity) 109 | .background(.ultraThinMaterial) 110 | .onAppear { 111 | withAnimation(.spring(response: 0.325, dampingFraction: 0.825, blendDuration: 0.2)) { 112 | self.showCard = true 113 | } 114 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 115 | self.focusState = .username 116 | } 117 | } 118 | .onChange(of: self.username) { 119 | self.signInEnabled = canSignIn(username: self.username, password: self.password) 120 | } 121 | .onChange(of: self.password) { 122 | self.signInEnabled = canSignIn(username: self.username, password: self.password) 123 | } 124 | } 125 | 126 | private func canSignIn(username: String, password: String) -> Bool { 127 | !username.isEmpty && !password.isEmpty 128 | } 129 | } 130 | 131 | #Preview { 132 | AppDependencyContainerView { 133 | SignInCardView() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Styles/ButtonStyles/BrightBorderedButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightBorderedButtonStyle.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct BrightBorderedButtonStyle: ButtonStyle { 11 | let color: Color 12 | let cornerRadius: CGFloat 13 | let borderWidth: CGFloat 14 | 15 | init(color: Color = .accentColor, cornerRadius: CGFloat = 16.0, borderWidth: CGFloat = 2.0) { 16 | self.color = color 17 | self.cornerRadius = cornerRadius 18 | self.borderWidth = borderWidth 19 | } 20 | 21 | func makeBody(configuration: Configuration) -> some View { 22 | configuration.label 23 | .foregroundColor(.white) 24 | .colorMultiply(self.color) 25 | .textStyle(.button) 26 | .overlay( 27 | RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) 28 | .stroke(self.color, lineWidth: self.borderWidth) 29 | .padding(1.0) 30 | ) 31 | .opacity(configuration.isPressed ? 0.5 : 1.0) 32 | } 33 | } 34 | 35 | extension ButtonStyle where Self == BrightBorderedButtonStyle { 36 | static var brightBorderedButton: BrightBorderedButtonStyle { 37 | BrightBorderedButtonStyle() 38 | } 39 | 40 | static func brightBorderedButton(color: Color = .accentColor, cornerRadius: CGFloat = 16.0, borderWidth: CGFloat = 2.0) -> BrightBorderedButtonStyle { 41 | BrightBorderedButtonStyle(color: color, cornerRadius: cornerRadius, borderWidth: borderWidth) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Styles/TextStyles/ButtonTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonTextStyle.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ButtonTextStyle: TextStyle { 11 | @ScaledMetric private var fontSize: CGFloat = 18.0 12 | 13 | func body(content: Content) -> some View { 14 | content 15 | .font(.system(size: self.fontSize, weight: .semibold, design: .rounded)) 16 | } 17 | } 18 | 19 | extension TextStyle where Self == ButtonTextStyle { 20 | static var button: Self { 21 | ButtonTextStyle() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Styles/TextStyles/TextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextStyle.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | protocol TextStyle: ViewModifier {} 11 | 12 | extension View { 13 | func textStyle(_ style: Style) -> some View { 14 | ModifiedContent(content: self, modifier: style) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Views/AppDependencyContainerView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDependencyContainerView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | import BusyIndicator 10 | 11 | typealias AppReduxStore = Redux.Store 12 | 13 | fileprivate let busyIndicatorService: BusyIndicatorServiceProtocol = { 14 | let config = BusyIndicatorConfiguration() 15 | config.showBusyIndicatorDelay = 100 16 | return BusyIndicatorService(configuration: config) 17 | }() 18 | 19 | fileprivate let colorService: ColorService = ColorService() 20 | 21 | struct AppDependencyContainerView: View { 22 | @StateObject var store: AppReduxStore = AppReduxStore { state, action in 23 | AppReducer.reduce(state, action: action) 24 | } middleware: { 25 | UserAuthMiddleware(busyIndicatorService: busyIndicatorService) 26 | UIMiddleware(colorService: colorService) 27 | } 28 | 29 | private let content: () -> Content 30 | private let startingActions: ((AppReduxStore) async -> Void)? 31 | 32 | init(@ViewBuilder content: @escaping () -> Content) { 33 | self.startingActions = nil 34 | self.content = content 35 | } 36 | 37 | init( 38 | startingActions: @escaping (AppReduxStore) async -> Void, 39 | @ViewBuilder content: @escaping () -> Content 40 | ) { 41 | self.startingActions = startingActions 42 | self.content = content 43 | } 44 | 45 | var body: some View { 46 | self.content() 47 | .reduxStore(self.store) 48 | .busyIndicator(busyIndicatorService.busyIndicator) 49 | .environmentObject(colorService) 50 | .task { 51 | await self.startingActions?(self.store) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Views/CardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct CardView: View where Content: View { 11 | let alignment: Alignment 12 | let color: Color 13 | let cornerRadius: CornerRadius 14 | let content: Content 15 | 16 | init( 17 | alignment: Alignment = .topLeading, 18 | color: Color, 19 | cornerRadius: CornerRadius = .medium, 20 | @ViewBuilder content: () -> Content) { 21 | self.alignment = alignment 22 | self.color = color 23 | self.cornerRadius = cornerRadius 24 | self.content = content() 25 | } 26 | 27 | var body: some View { 28 | ZStack(alignment: self.alignment) { 29 | RoundedRectangle(cornerRadius: self.cornerRadius.value, style: .continuous) 30 | .fill(self.color) 31 | self.content 32 | } 33 | } 34 | } 35 | 36 | extension CardView { 37 | enum CornerRadius { 38 | case small 39 | case medium 40 | case large 41 | case custom(CGFloat) 42 | 43 | var value: CGFloat { 44 | switch self { 45 | case .small: return 4.0 46 | case .medium: return 8.0 47 | case .large: return 16.0 48 | case .custom(let value): return value 49 | } 50 | } 51 | } 52 | } 53 | 54 | #Preview { 55 | CardView( 56 | color: Color.green, 57 | cornerRadius: .medium) { 58 | Text("Card contents.") 59 | .padding() 60 | .background(Color.orange) 61 | .padding() 62 | } 63 | .clipped() 64 | .shadow(radius: 10) 65 | .padding() 66 | } 67 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUI/UI/Views/VisualEffectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisualEffectView.swift 3 | // Redux.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/4/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct VisualEffectView: UIViewRepresentable { 11 | let effect: UIVisualEffect? 12 | func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() } 13 | func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = self.effect } 14 | } 15 | 16 | struct ProgressiveVisualEffectView: UIViewRepresentable { 17 | let effect: UIVisualEffect 18 | let intensity: CGFloat 19 | 20 | func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { 21 | UIProgressiveVisualEffectView(effect: self.effect, intensity: self.intensity) 22 | } 23 | 24 | func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = self.effect } 25 | } 26 | 27 | final class UIProgressiveVisualEffectView: UIVisualEffectView { 28 | private let theEffect: UIVisualEffect 29 | private let customIntensity: CGFloat 30 | private var animator: UIViewPropertyAnimator? 31 | 32 | /// Create visual effect view with given effect and its intensity 33 | /// 34 | /// - Parameters: 35 | /// - effect: visual effect, eg UIBlurEffect(style: .dark) 36 | /// - intensity: custom intensity from 0.0 (no effect) to 1.0 (full effect) using linear scale 37 | init(effect: UIVisualEffect, intensity: CGFloat) { 38 | self.theEffect = effect 39 | self.customIntensity = intensity 40 | super.init(effect: nil) 41 | } 42 | 43 | required init?(coder aDecoder: NSCoder) { nil } 44 | 45 | deinit { 46 | self.animator?.stopAnimation(true) 47 | } 48 | 49 | override func draw(_ rect: CGRect) { 50 | super.draw(rect) 51 | effect = nil 52 | self.animator?.stopAnimation(true) 53 | self.animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in 54 | self.effect = self.theEffect 55 | } 56 | self.animator?.fractionComplete = self.customIntensity 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Extensions/String+ExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+ExtensionsTests.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class String_Extensions_Tests: XCTestCase { 12 | func test_when_initialized_from_empty_extension() { 13 | let string: String = .empty 14 | XCTAssertEqual(string, "") 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Extensions/SwiftUI/EdgeInsets+ExtensionsTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsets+ExtensionsTests.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/24/24. 6 | // 7 | 8 | import XCTest 9 | import SwiftUI 10 | @testable import Redux_Demo_SwiftUI 11 | 12 | class EdgeInsets_Extensions_Tests: XCTestCase { 13 | func test_when_initialized_with_all() { 14 | let expected = 10.0 15 | let subject = EdgeInsets(all: expected) 16 | XCTAssertEqual(subject.leading, expected) 17 | XCTAssertEqual(subject.top, expected) 18 | XCTAssertEqual(subject.trailing, expected) 19 | XCTAssertEqual(subject.bottom, expected) 20 | } 21 | 22 | func test_when_initialized_with_horizontal_and_vertical() { 23 | let horizontal = 10.0 24 | let vertical = 15.0 25 | let subject = EdgeInsets(horizontal: horizontal, vertical: vertical) 26 | XCTAssertEqual(subject.leading, horizontal) 27 | XCTAssertEqual(subject.top, vertical) 28 | XCTAssertEqual(subject.trailing, horizontal) 29 | XCTAssertEqual(subject.bottom, vertical) 30 | } 31 | 32 | func test_when_initialized_with_top_and_remaining() { 33 | let top = 10.0 34 | let remaining = 15.0 35 | let subject = EdgeInsets(top: top, and: remaining) 36 | XCTAssertEqual(subject.leading, remaining) 37 | XCTAssertEqual(subject.top, top) 38 | XCTAssertEqual(subject.trailing, remaining) 39 | XCTAssertEqual(subject.bottom, remaining) 40 | } 41 | 42 | func test_when_initialized_from_all_extension() { 43 | let expected = 10.0 44 | let subject = EdgeInsets.all(expected) 45 | XCTAssertEqual(subject.leading, expected) 46 | XCTAssertEqual(subject.top, expected) 47 | XCTAssertEqual(subject.trailing, expected) 48 | XCTAssertEqual(subject.bottom, expected) 49 | } 50 | 51 | func test_when_initialized_from_horizontal_and_vertical_extension() { 52 | let horizontal = 10.0 53 | let vertical = 15.0 54 | let subject = EdgeInsets.horizontal(horizontal, vertical: vertical) 55 | XCTAssertEqual(subject.leading, horizontal) 56 | XCTAssertEqual(subject.top, vertical) 57 | XCTAssertEqual(subject.trailing, horizontal) 58 | XCTAssertEqual(subject.bottom, vertical) 59 | } 60 | 61 | func test_when_initialized_from_top_and_remaining_extension() { 62 | let top = 10.0 63 | let remaining = 15.0 64 | let subject = EdgeInsets.top(top, and: remaining) 65 | XCTAssertEqual(subject.leading, remaining) 66 | XCTAssertEqual(subject.top, top) 67 | XCTAssertEqual(subject.trailing, remaining) 68 | XCTAssertEqual(subject.bottom, remaining) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Middleware/UIMiddleware/UIMiddlewareTest+ColorWizard.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIMiddlewareTest+ColorWizard.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class UIMiddleware_when_ui_colorWizard: UIMiddlewareTest { 12 | func test_and_onBack_is_executed_then_input_action_is_returned() async { 13 | let action = Actions.ui(.colorWizard(.onBack)) 14 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 15 | XCTAssertEqual(result, .action(action)) 16 | } 17 | 18 | func test_and_onNext_is_executed_then_input_action_is_returned() async { 19 | let action = Actions.ui(.colorWizard(.onNext)) 20 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 21 | XCTAssertEqual(result, .action(action)) 22 | } 23 | } 24 | 25 | class UIMiddleware_when_ui_colorWizard_onFinished_is_executed: UIMiddlewareTest { 26 | var action: Actions! 27 | var result: Redux.MiddlewareResult! 28 | 29 | override func setUp() async throws { 30 | try await super.setUp() 31 | self.action = .uiAction(.colorWizard(.onFinish)) 32 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 33 | } 34 | 35 | func test_then_result_is_handled() { 36 | XCTAssertEqual(self.result, .handled) 37 | } 38 | 39 | func test_then_actions_cache_contains_expected_actions() { 40 | let expected: [Actions] = [ 41 | .uiAction(.landingScreen(.setColorWizardState(nil))), 42 | ] 43 | XCTAssertEqual(self.actionsCache.actions, expected) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Middleware/UIMiddleware/UIMiddlewareTest+General.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIMiddlewareTest+General.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class UIMiddleware_when_ui_general_presentFailedToSignInAlert_is_executed: UIMiddlewareTest { 12 | func test_then_input_action_is_returned() async { 13 | let username = "test.username" 14 | let action = Actions.ui(.general(.presentFailedToSignInAlert(error: TestError.test, username: username))) 15 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 16 | XCTAssertEqual(result, .action(action)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Middleware/UIMiddleware/UIMiddlewareTest+LandingScreen.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIMiddlewareTest+LandingScreen.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class UIMiddleware_when_ui_landingScreen: UIMiddlewareTest { 12 | func test_and_onPulseTapped_is_executed_then_input_action_is_returned() async { 13 | let action = Actions.uiAction(.landingScreen(.onPulseTapped)) 14 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 15 | XCTAssertEqual(result, .action(action)) 16 | } 17 | 18 | func test_and_onSignInTapped_is_executed_then_input_action_is_returned() async { 19 | let action = Actions.uiAction(.landingScreen(.onSignInTapped)) 20 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 21 | XCTAssertEqual(result, .action(action)) 22 | } 23 | 24 | func test_and_dismissSignInModal_is_executed_then_input_action_is_returned() async { 25 | let action = Actions.uiAction(.landingScreen(.dismissSignInModal)) 26 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 27 | XCTAssertEqual(result, .action(action)) 28 | } 29 | 30 | func test_and_dismissPulseView_is_executed_then_input_action_is_returned() async { 31 | let action = Actions.uiAction(.landingScreen(.dismissPulseView)) 32 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 33 | XCTAssertEqual(result, .action(action)) 34 | } 35 | 36 | func test_and_setColorWizardState_is_executed_then_input_action_is_returned() async { 37 | let action = Actions.uiAction(.landingScreen(.setColorWizardState(nil))) 38 | let result = await self.subject(action: action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 39 | XCTAssertEqual(result, .action(action)) 40 | } 41 | } 42 | 43 | class UIMiddleware_when_ui_landingScreen_onSignOutTapped_is_executed: UIMiddlewareTest { 44 | var action: Actions! 45 | var result: Redux.MiddlewareResult! 46 | 47 | override func setUp() async throws { 48 | try await super.setUp() 49 | self.action = .uiAction(.landingScreen(.onSignOutTapped)) 50 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 51 | } 52 | 53 | func test_then_result_is_handled() { 54 | XCTAssertEqual(self.result, .handled) 55 | } 56 | 57 | func test_then_actions_cache_contains_expected_actions() { 58 | let expected: [Actions] = [ 59 | .userAction(.signOut), 60 | ] 61 | XCTAssertEqual(self.actionsCache.actions, expected) 62 | } 63 | } 64 | 65 | class UIMiddleware_when_ui_landingScreen_onStartColorWizard_is_executed: UIMiddlewareTest { 66 | var action: Actions! 67 | var result: Redux.MiddlewareResult! 68 | 69 | override func setUp() async throws { 70 | try await super.setUp() 71 | self.action = .uiAction(.landingScreen(.onStartColorWizard)) 72 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 73 | } 74 | 75 | func test_then_result_is_handled() { 76 | XCTAssertEqual(self.result, .handled) 77 | } 78 | 79 | func test_then_actions_cache_contains_expected_actions() { 80 | let config = ColorWizardConfiguration.mock() 81 | let screens = config.pages.compactMap { page in 82 | if let color = page.color { 83 | return UIState.ColorWizardScreenState(title: page.title, data: .color(color)) 84 | } else if let colors = page.colors { 85 | return UIState.ColorWizardScreenState(title: page.title, data: .summary(colors)) 86 | } else { 87 | return nil 88 | } 89 | } 90 | let colorWizardState = UIState.ColorWizardState( 91 | screens: screens, currentScreenIndex: 0, 92 | canMoveBack: false, 93 | canMoveNext: screens.count > 1, 94 | canFinish: screens.count == 1) 95 | 96 | let expected: [Actions] = [ 97 | .uiAction(.landingScreen(.setColorWizardState(colorWizardState))) 98 | ] 99 | XCTAssertEqual(self.actionsCache.actions, expected) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Middleware/UIMiddleware/UIMiddlewareTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIMiddlewareTest.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class UIMiddlewareTest: XCTestCase { 12 | var actionsCache: ActionsCacheReducer! 13 | var colorService: ColorService! 14 | var store: AppReduxStore! 15 | var subject: UIMiddleware! 16 | 17 | override func setUp() async throws { 18 | try await super.setUp() 19 | self.colorService = ColorService() 20 | self.subject = UIMiddleware(colorService: self.colorService) 21 | self.actionsCache = ActionsCacheReducer() 22 | self.store = AppReduxStore { [weak actionsCache] state, action in 23 | actionsCache?.append(action) 24 | return AppReducer.reduce(state, action: action) 25 | } middleware: { 26 | self.subject 27 | } 28 | } 29 | } 30 | 31 | extension UIMiddlewareTest { 32 | enum TestError: LocalizedError { 33 | case test 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Middleware/UserAuthMiddleware/UserAuthMiddlewareTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UserAuthMiddlewareTest.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | import BusyIndicator 11 | 12 | class UserAuthMiddlewareTest: XCTestCase { 13 | var busyIndicatorService: BusyIndicatorService! 14 | var actionsCache: ActionsCacheReducer! 15 | var store: AppReduxStore! 16 | var subject: UserAuthMiddleware! 17 | 18 | override func setUp() async throws { 19 | try await super.setUp() 20 | self.busyIndicatorService = BusyIndicatorService() 21 | self.subject = UserAuthMiddleware(busyIndicatorService: self.busyIndicatorService) 22 | self.actionsCache = ActionsCacheReducer() 23 | self.store = AppReduxStore { [weak actionsCache] state, action in 24 | actionsCache?.append(action) 25 | return AppReducer.reduce(state, action: action) 26 | } middleware: { 27 | self.subject 28 | } 29 | } 30 | } 31 | 32 | class UserAuthMiddleware_when_signIn_executed_and_username_and_password_are_empty: UserAuthMiddlewareTest { 33 | var action: Actions! 34 | var result: Redux.MiddlewareResult! 35 | 36 | override func setUp() async throws { 37 | try await super.setUp() 38 | self.action = .userAction(.signIn(username: .empty, password: .empty)) 39 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 40 | } 41 | 42 | func test_then_result_is_user_setFailedToSignIn() { 43 | XCTAssertEqual(self.result, .action(Actions.userAction(.setFailedToSignIn( 44 | error: UserAuthMiddleware.Errors.failedToSignIn( 45 | username: .empty, password: .empty), 46 | username: .empty, 47 | password: .empty))) 48 | ) 49 | } 50 | } 51 | 52 | class UserAuthMiddleware_when_signIn_executed_and_username_and_password_are_valid: UserAuthMiddlewareTest { 53 | var action: Actions! 54 | var result: Redux.MiddlewareResult! 55 | 56 | override func setUp() async throws { 57 | try await super.setUp() 58 | self.action = .userAction(.signIn(username: "username", password: "password")) 59 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 60 | } 61 | 62 | func test_then_result_is_handled() { 63 | XCTAssertEqual(self.result, .handled) 64 | } 65 | 66 | func test_then_actions_cache_contains_expected_actions() { 67 | let expected: [Actions] = [ 68 | .userAction(.signInSuccessful(username: "username")), 69 | .uiAction(.landingScreen(.dismissSignInModal)), 70 | ] 71 | XCTAssertEqual(self.actionsCache.actions, expected) 72 | } 73 | } 74 | 75 | class UserAuthMiddleware_when_setFailedToSignIn_executed: UserAuthMiddlewareTest { 76 | var action: Actions! 77 | var result: Redux.MiddlewareResult! 78 | 79 | override func setUp() async throws { 80 | try await super.setUp() 81 | self.action = .userAction(.setFailedToSignIn(error: UserAuthMiddleware.Errors.failedToSignIn(username: "username", password: "password"), username: "username", password: "password")) 82 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 83 | } 84 | 85 | func test_then_result_is_handled() { 86 | XCTAssertEqual(self.result, .handled) 87 | } 88 | 89 | func test_then_actions_cache_contains_expected_actions() { 90 | let expected: [Actions] = [ 91 | .uiAction(.landingScreen(.dismissSignInModal)), 92 | .uiAction(.general(.presentFailedToSignInAlert(error: UserAuthMiddleware.Errors.failedToSignIn(username: "username", password: "password"), username: "username"))), 93 | ] 94 | XCTAssertEqual(self.actionsCache.actions, expected) 95 | } 96 | } 97 | 98 | class UserAuthMiddleware_when_signInSuccessful_executed: UserAuthMiddlewareTest { 99 | var action: Actions! 100 | var result: Redux.MiddlewareResult! 101 | 102 | override func setUp() async throws { 103 | try await super.setUp() 104 | self.action = .userAction(.signInSuccessful(username: "username")) 105 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 106 | } 107 | 108 | func test_then_result_is_action() { 109 | XCTAssertEqual(self.result, .action(self.action)) 110 | } 111 | } 112 | 113 | class UserAuthMiddleware_when_signOut_executed: UserAuthMiddlewareTest { 114 | var action: Actions! 115 | var result: Redux.MiddlewareResult! 116 | 117 | override func setUp() async throws { 118 | try await super.setUp() 119 | self.action = .userAction(.signOut) 120 | self.result = await self.subject(action: self.action, dispatcher: self.store.eraseToAnyStoreDispatcher()) 121 | } 122 | 123 | func test_then_result_is_action() { 124 | XCTAssertEqual(self.result, .action(self.action)) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/MocksAndTestObjects/ActionsCacheReducer.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestAppReduxStore.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/25/24. 6 | // 7 | 8 | import Foundation 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class ActionsCacheReducer { 12 | private(set) var actions: [Actions] = [] 13 | 14 | func append(_ action: Actions) { 15 | self.actions.append(action) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Reducers+Actions/UIReducer+ColorWizardTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIReducer+ColorWizardTests.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/27/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class UIReducer_ColorWizardTests: XCTestCase { 12 | var colorService: ColorService! 13 | var colorWizardState: UIState.ColorWizardState! 14 | 15 | override func setUp() async throws { 16 | try await super.setUp() 17 | self.colorService = ColorService() 18 | self.colorWizardState = await self.colorService.fetchColorWizardState() 19 | } 20 | 21 | func test_when_state_is_nil_all_actions_return_nil() { 22 | XCTAssertNil(UIReducer.ColorWizardReducer.reduce(nil, action: .onBack)) 23 | XCTAssertNil(UIReducer.ColorWizardReducer.reduce(nil, action: .onNext)) 24 | XCTAssertNil(UIReducer.ColorWizardReducer.reduce(nil, action: .onFinish)) 25 | } 26 | 27 | func test_when_state_is_not_nil() { 28 | var actual = UIReducer.ColorWizardReducer.reduce(self.colorWizardState, action: .onBack) 29 | XCTAssertEqual(actual, self.colorWizardState) 30 | 31 | actual = UIReducer.ColorWizardReducer.reduce(self.colorWizardState, action: .onNext) 32 | var expectedScreens = self.colorWizardState.screens 33 | expectedScreens[0].next = .push 34 | XCTAssertEqual( 35 | actual, 36 | UIState.ColorWizardState( 37 | id: self.colorWizardState.id, 38 | screens: expectedScreens, 39 | currentScreenIndex: 1, 40 | canMoveBack: true, 41 | canMoveNext: true, 42 | canFinish: false)) 43 | 44 | actual = UIReducer.ColorWizardReducer.reduce(actual, action: .onBack) 45 | expectedScreens = self.colorWizardState.screens 46 | expectedScreens[0].next = .none 47 | XCTAssertEqual( 48 | actual, 49 | UIState.ColorWizardState( 50 | id: self.colorWizardState.id, 51 | screens: expectedScreens, 52 | currentScreenIndex: 0, 53 | canMoveBack: false, 54 | canMoveNext: true, 55 | canFinish: false)) 56 | 57 | actual = UIReducer.ColorWizardReducer.reduce(self.colorWizardState, action: .onNext) 58 | actual = UIReducer.ColorWizardReducer.reduce(actual, action: .onNext) 59 | actual = UIReducer.ColorWizardReducer.reduce(actual, action: .onNext) 60 | actual = UIReducer.ColorWizardReducer.reduce(actual, action: .onNext) 61 | actual = UIReducer.ColorWizardReducer.reduce(actual, action: .onNext) 62 | expectedScreens = self.colorWizardState.screens 63 | expectedScreens[0].next = .push 64 | expectedScreens[1].next = .push 65 | expectedScreens[2].next = .push 66 | expectedScreens[3].next = .push 67 | expectedScreens[4].next = .push 68 | XCTAssertEqual( 69 | actual, 70 | UIState.ColorWizardState( 71 | id: self.colorWizardState.id, 72 | screens: expectedScreens, 73 | currentScreenIndex: 5, 74 | canMoveBack: true, 75 | canMoveNext: false, 76 | canFinish: true)) 77 | 78 | actual = UIReducer.ColorWizardReducer.reduce(actual, action: .onNext) 79 | XCTAssertEqual( 80 | actual, 81 | UIState.ColorWizardState( 82 | id: self.colorWizardState.id, 83 | screens: expectedScreens, 84 | currentScreenIndex: 5, 85 | canMoveBack: true, 86 | canMoveNext: false, 87 | canFinish: true)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Redux_Demo_SwiftUITests.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// Redux_Demo_SwiftUITests.swift 3 | //// Redux.Demo.SwiftUITests 4 | //// 5 | //// Created by Jason Lew-Rapai on 3/27/24. 6 | //// 7 | // 8 | //import XCTest 9 | //@testable import Redux_Demo_SwiftUI 10 | // 11 | //final class Redux_Demo_SwiftUITests: XCTestCase { 12 | // 13 | // override func setUpWithError() throws { 14 | // // Put setup code here. This method is called before the invocation of each test method in the class. 15 | // } 16 | // 17 | // override func tearDownWithError() throws { 18 | // // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | // } 20 | // 21 | // func testExample() throws { 22 | // // This is an example of a functional test case. 23 | // // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | // // Any test you write for XCTest can be annotated as throws and async. 25 | // // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. 26 | // // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. 27 | // } 28 | // 29 | // func testPerformanceExample() throws { 30 | // // This is an example of a performance test case. 31 | // self.measure { 32 | // // Put the code you want to measure the time of here. 33 | // } 34 | // } 35 | // 36 | //} 37 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUITests/Services/ColorServiceTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorServiceTests.swift 3 | // Redux.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 4/26/24. 6 | // 7 | 8 | import XCTest 9 | @testable import Redux_Demo_SwiftUI 10 | 11 | class ColorServiceTest: XCTestCase { 12 | var colorService: ColorService! 13 | 14 | override func setUp() async throws { 15 | try await super.setUp() 16 | self.colorService = ColorService() 17 | } 18 | } 19 | 20 | class ColorService_when_getNextColor_executed: ColorServiceTest { 21 | func test_then_result_is_expected() { 22 | XCTAssertEqual(self.colorService.getNextColor(), .blue) 23 | XCTAssertEqual(self.colorService.getNextColor(), .green) 24 | XCTAssertEqual(self.colorService.getNextColor(), .orange) 25 | XCTAssertEqual(self.colorService.getNextColor(), .pink) 26 | XCTAssertEqual(self.colorService.getNextColor(), .purple) 27 | XCTAssertEqual(self.colorService.getNextColor(), .red) 28 | XCTAssertEqual(self.colorService.getNextColor(), .yellow) 29 | XCTAssertEqual(self.colorService.getNextColor(), .blue) 30 | XCTAssertEqual(self.colorService.getNextColor(), .green) 31 | XCTAssertEqual(self.colorService.getNextColor(), .orange) 32 | XCTAssertEqual(self.colorService.getNextColor(), .pink) 33 | XCTAssertEqual(self.colorService.getNextColor(), .purple) 34 | XCTAssertEqual(self.colorService.getNextColor(), .red) 35 | XCTAssertEqual(self.colorService.getNextColor(), .yellow) 36 | } 37 | } 38 | 39 | class ColorService_when_generateColors_executed: ColorServiceTest { 40 | func test_then_result_is_expected() async throws { 41 | let result = try await self.colorService 42 | .generateColors(every: 0.01) 43 | .collect(.byTime(RunLoop.main, .seconds(1))) 44 | .eraseToAnyPublisher() 45 | .async() 46 | result.forEach { 47 | XCTAssertTrue(ColorModel.allCases.contains($0)) 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUIUITests/Redux_Demo_SwiftUIUITests.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// Redux_Demo_SwiftUIUITests.swift 3 | //// Redux.Demo.SwiftUIUITests 4 | //// 5 | //// Created by Jason Lew-Rapai on 3/27/24. 6 | //// 7 | // 8 | //import XCTest 9 | // 10 | //final class Redux_Demo_SwiftUIUITests: XCTestCase { 11 | // 12 | // override func setUpWithError() throws { 13 | // // Put setup code here. This method is called before the invocation of each test method in the class. 14 | // 15 | // // In UI tests it is usually best to stop immediately when a failure occurs. 16 | // continueAfterFailure = false 17 | // 18 | // // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | // } 20 | // 21 | // override func tearDownWithError() throws { 22 | // // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | // } 24 | // 25 | // func testExample() throws { 26 | // // UI tests must launch the application that they test. 27 | // let app = XCUIApplication() 28 | // app.launch() 29 | // 30 | // // Use XCTAssert and related functions to verify your tests produce the correct results. 31 | // } 32 | // 33 | // func testLaunchPerformance() throws { 34 | // if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 35 | // // This measures how long it takes to launch your application. 36 | // measure(metrics: [XCTApplicationLaunchMetric()]) { 37 | // XCUIApplication().launch() 38 | // } 39 | // } 40 | // } 41 | //} 42 | -------------------------------------------------------------------------------- /Redux.Demo.SwiftUIUITests/Redux_Demo_SwiftUIUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | //// 2 | //// Redux_Demo_SwiftUIUITestsLaunchTests.swift 3 | //// Redux.Demo.SwiftUIUITests 4 | //// 5 | //// Created by Jason Lew-Rapai on 3/27/24. 6 | //// 7 | // 8 | //import XCTest 9 | // 10 | //final class Redux_Demo_SwiftUIUITestsLaunchTests: XCTestCase { 11 | // 12 | // override class var runsForEachTargetApplicationUIConfiguration: Bool { 13 | // true 14 | // } 15 | // 16 | // override func setUpWithError() throws { 17 | // continueAfterFailure = false 18 | // } 19 | // 20 | // func testLaunch() throws { 21 | // let app = XCUIApplication() 22 | // app.launch() 23 | // 24 | // // Insert steps here to perform after app launch but before taking a screenshot, 25 | // // such as logging into a test account or navigating somewhere in the app 26 | // 27 | // let attachment = XCTAttachment(screenshot: app.screenshot()) 28 | // attachment.name = "Launch Screen" 29 | // attachment.lifetime = .keepAlways 30 | // add(attachment) 31 | // } 32 | //} 33 | --------------------------------------------------------------------------------