├── .DS_Store ├── .gitattributes ├── .gitignore ├── LICENSE ├── MVVM.Demo.SwiftUI.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcshareddata │ └── xcschemes │ └── MVVM.Demo.SwiftUI.xcscheme ├── MVVM.Demo.SwiftUI ├── Architecture │ ├── AppAssembler.swift │ ├── CoordinatorAssembly.swift │ ├── ServiceAssembly.swift │ ├── ViewModel.swift │ └── ViewModelAssembly.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json ├── Core │ ├── Combine │ │ ├── CancelBag.swift │ │ ├── Just+Void.swift │ │ ├── PassthroughSubject+Extensions.swift │ │ ├── Publisher+DefinedScheduler.swift │ │ └── Publisher+Extensions.swift │ ├── Extensions │ │ ├── Resolver+Resolved.swift │ │ ├── String+Extensions.swift │ │ └── UIApplication+EndEditing.swift │ ├── HapticFeedbackProvider.swift │ └── SwiftUI │ │ ├── Button+Extensions.swift │ │ ├── Color+SystemColors.swift │ │ ├── EdgeInsets+Extensions.swift │ │ ├── ObjectNavigationStack.swift │ │ └── View+OnReceive.swift ├── MVVM_Demo_SwiftUIApp.swift ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json ├── Services │ ├── AlertService │ │ ├── AlertService.AlertPackage+View.swift │ │ └── AlertService.swift │ ├── AuthenticationService.swift │ └── ColorService.swift └── UI │ ├── AppRootCoordinator │ ├── AppRootCoordinator.swift │ └── AppRootCoordinatorView.swift │ ├── ColorWizard │ ├── ColorWizardCoordinator.swift │ ├── ColorWizardCoordinatorView.swift │ ├── Configuration │ │ ├── ColorWizardConfiguration+Mock.swift │ │ ├── ColorWizardConfiguration.swift │ │ └── ColorWizardConfigurationViewModel.swift │ └── Content │ │ ├── ColorWizardContentView.swift │ │ └── ColorWizardContentViewModel.swift │ ├── InfiniteCards │ ├── InfiniteCardsView.swift │ └── InfiniteCardsViewModel.swift │ ├── Landing │ ├── LandingView.swift │ └── LandingViewModel.swift │ ├── Pulse │ ├── PulseView.swift │ └── PulseViewModel.swift │ ├── SignIn │ ├── SignInView.swift │ └── SignInViewModel.swift │ ├── Styles │ ├── ButtonStyles │ │ └── BrightBorderedButtonStyle.swift │ └── TextStyles │ │ ├── ButtonTextStyle.swift │ │ └── TextStyle.swift │ └── Views │ ├── CardView.swift │ ├── Modifiers │ └── AlertManager+Environment.swift │ └── VisualEffectView.swift ├── MVVM.Demo.SwiftUITests ├── MVVM_Demo_SwiftUITests.swift ├── Services │ ├── AlertService │ │ ├── AlertService+Tests.swift │ │ └── AlertService.AlertPackage.Button+Tests.swift │ ├── AuthenticationService+Tests.swift │ └── ColorService+Tests.swift └── TestExtensions │ ├── AnyPublisher+AwaitResults.swift │ └── Result+TestExtensions.swift ├── MVVM.Demo.SwiftUIUITests ├── MVVM_Demo_SwiftUIUITests.swift └── MVVM_Demo_SwiftUIUITestsLaunchTests.swift └── README.md /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonjrr/MVVM.Demo.SwiftUI/dca672151445f410530f802de2800f6467f978f6/.DS_Store -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 55; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 6FD86CD9293DE9060052C759 /* ObjectNavigationStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FD86CD8293DE9060052C759 /* ObjectNavigationStack.swift */; }; 11 | 6FD86CDC2940033C0052C759 /* BusyIndicator in Frameworks */ = {isa = PBXBuildFile; productRef = 6FD86CDB2940033C0052C759 /* BusyIndicator */; }; 12 | 6FDA2F112BE0573700A83DDB /* InfiniteCardsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA2F102BE0573700A83DDB /* InfiniteCardsView.swift */; }; 13 | 6FDA2F132BE0575000A83DDB /* InfiniteCardsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FDA2F122BE0575000A83DDB /* InfiniteCardsViewModel.swift */; }; 14 | 6FF089B92BAC9387001C454E /* Resolver+Resolved.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FF089B82BAC9387001C454E /* Resolver+Resolved.swift */; }; 15 | 9647788B2774F0EE002203DE /* AnyPublisher+AwaitResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9647788A2774F0EE002203DE /* AnyPublisher+AwaitResults.swift */; }; 16 | 964778902774F240002203DE /* AuthenticationService+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9647788F2774F240002203DE /* AuthenticationService+Tests.swift */; }; 17 | 964778922774F64A002203DE /* Result+TestExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 964778912774F64A002203DE /* Result+TestExtensions.swift */; }; 18 | 9665D9552745C1D60055F1F6 /* ColorWizardCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9542745C1D60055F1F6 /* ColorWizardCoordinatorView.swift */; }; 19 | 9665D9572745C1F10055F1F6 /* ColorWizardCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9562745C1F10055F1F6 /* ColorWizardCoordinator.swift */; }; 20 | 9665D95F2745C2E00055F1F6 /* ColorWizardContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D95E2745C2E00055F1F6 /* ColorWizardContentView.swift */; }; 21 | 9665D9612745C2FA0055F1F6 /* ColorWizardContentViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9602745C2FA0055F1F6 /* ColorWizardContentViewModel.swift */; }; 22 | 9665D9632745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9622745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift */; }; 23 | 9665D9652745C3700055F1F6 /* ColorWizardConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9642745C3700055F1F6 /* ColorWizardConfiguration.swift */; }; 24 | 9665D9682745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9665D9672745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift */; }; 25 | 968D344D27CD941700340C18 /* AlertService.AlertPackage+View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968D344C27CD941700340C18 /* AlertService.AlertPackage+View.swift */; }; 26 | 968D344F27CD978600340C18 /* ColorService+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 968D344E27CD978600340C18 /* ColorService+Tests.swift */; }; 27 | 96B4E06527432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E06427432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift */; }; 28 | 96B4E06927432D6200EC88B3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96B4E06827432D6200EC88B3 /* Assets.xcassets */; }; 29 | 96B4E06C27432D6200EC88B3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 96B4E06B27432D6200EC88B3 /* Preview Assets.xcassets */; }; 30 | 96B4E07627432D6300EC88B3 /* MVVM_Demo_SwiftUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E07527432D6300EC88B3 /* MVVM_Demo_SwiftUITests.swift */; }; 31 | 96B4E08027432D6300EC88B3 /* MVVM_Demo_SwiftUIUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E07F27432D6300EC88B3 /* MVVM_Demo_SwiftUIUITests.swift */; }; 32 | 96B4E08227432D6300EC88B3 /* MVVM_Demo_SwiftUIUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E08127432D6300EC88B3 /* MVVM_Demo_SwiftUIUITestsLaunchTests.swift */; }; 33 | 96B4E09127432E1100EC88B3 /* ViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E09027432E1100EC88B3 /* ViewModel.swift */; }; 34 | 96B4E09327432E4000EC88B3 /* HapticFeedbackProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E09227432E4000EC88B3 /* HapticFeedbackProvider.swift */; }; 35 | 96B4E09627432E6800EC88B3 /* CancelBag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E09527432E6800EC88B3 /* CancelBag.swift */; }; 36 | 96B4E09827432E7D00EC88B3 /* Just+Void.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E09727432E7D00EC88B3 /* Just+Void.swift */; }; 37 | 96B4E09A27432E9300EC88B3 /* Publisher+DefinedScheduler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E09927432E9300EC88B3 /* Publisher+DefinedScheduler.swift */; }; 38 | 96B4E09C27432EB100EC88B3 /* Publisher+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E09B27432EB100EC88B3 /* Publisher+Extensions.swift */; }; 39 | 96B4E0A227432EEE00EC88B3 /* CombineExt in Frameworks */ = {isa = PBXBuildFile; productRef = 96B4E0A127432EEE00EC88B3 /* CombineExt */; }; 40 | 96B4E0A527432EFC00EC88B3 /* Swinject in Frameworks */ = {isa = PBXBuildFile; productRef = 96B4E0A427432EFC00EC88B3 /* Swinject */; }; 41 | 96B4E0A727432F1C00EC88B3 /* PassthroughSubject+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0A627432F1C00EC88B3 /* PassthroughSubject+Extensions.swift */; }; 42 | 96B4E0AC27432F8900EC88B3 /* String+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0AB27432F8900EC88B3 /* String+Extensions.swift */; }; 43 | 96B4E0AE27432FAB00EC88B3 /* UIApplication+EndEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0AD27432FAB00EC88B3 /* UIApplication+EndEditing.swift */; }; 44 | 96B4E0B027432FD000EC88B3 /* Button+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0AF27432FD000EC88B3 /* Button+Extensions.swift */; }; 45 | 96B4E0B327432FEB00EC88B3 /* EdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0B227432FEB00EC88B3 /* EdgeInsets+Extensions.swift */; }; 46 | 96B4E0B92743303F00EC88B3 /* View+OnReceive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0B82743303F00EC88B3 /* View+OnReceive.swift */; }; 47 | 96B4E0BE2743309B00EC88B3 /* AppAssembler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0BD2743309B00EC88B3 /* AppAssembler.swift */; }; 48 | 96B4E0C0274330B400EC88B3 /* CoordinatorAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0BF274330B400EC88B3 /* CoordinatorAssembly.swift */; }; 49 | 96B4E0C2274330CB00EC88B3 /* ServiceAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0C1274330CB00EC88B3 /* ServiceAssembly.swift */; }; 50 | 96B4E0C4274330E600EC88B3 /* ViewModelAssembly.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0C3274330E600EC88B3 /* ViewModelAssembly.swift */; }; 51 | 96B4E0C82743313900EC88B3 /* AppRootCoordinatorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0C72743313900EC88B3 /* AppRootCoordinatorView.swift */; }; 52 | 96B4E0CA2743314700EC88B3 /* AppRootCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0C92743314700EC88B3 /* AppRootCoordinator.swift */; }; 53 | 96B4E0CD274331EE00EC88B3 /* AuthenticationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0CC274331EE00EC88B3 /* AuthenticationService.swift */; }; 54 | 96B4E0CF274335A900EC88B3 /* ColorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0CE274335A900EC88B3 /* ColorService.swift */; }; 55 | 96B4E0D1274336EE00EC88B3 /* AlertService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0D0274336EE00EC88B3 /* AlertService.swift */; }; 56 | 96B4E0D42743382B00EC88B3 /* LandingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0D32743382B00EC88B3 /* LandingView.swift */; }; 57 | 96B4E0D62743384F00EC88B3 /* LandingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0D52743384F00EC88B3 /* LandingViewModel.swift */; }; 58 | 96B4E0DA2743390E00EC88B3 /* BrightBorderedButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0D92743390E00EC88B3 /* BrightBorderedButtonStyle.swift */; }; 59 | 96B4E0DD2743393E00EC88B3 /* TextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0DC2743393E00EC88B3 /* TextStyle.swift */; }; 60 | 96B4E0DF2743396600EC88B3 /* ButtonTextStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0DE2743396600EC88B3 /* ButtonTextStyle.swift */; }; 61 | 96B4E0E227437CA200EC88B3 /* SignInView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0E127437CA200EC88B3 /* SignInView.swift */; }; 62 | 96B4E0E427437CBA00EC88B3 /* SignInViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0E327437CB900EC88B3 /* SignInViewModel.swift */; }; 63 | 96B4E0E727437D0600EC88B3 /* VisualEffectView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0E627437D0600EC88B3 /* VisualEffectView.swift */; }; 64 | 96B4E0E927437D5300EC88B3 /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0E827437D5300EC88B3 /* CardView.swift */; }; 65 | 96B4E0EB274386FA00EC88B3 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0EA274386FA00EC88B3 /* Color+SystemColors.swift */; }; 66 | 96B4E0EE274424F600EC88B3 /* PulseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0ED274424F600EC88B3 /* PulseView.swift */; }; 67 | 96B4E0F0274424FE00EC88B3 /* PulseViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96B4E0EF274424FE00EC88B3 /* PulseViewModel.swift */; }; 68 | 96F46E0627DFAF2700F20CDC /* AlertService+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F46E0527DFAF2700F20CDC /* AlertService+Tests.swift */; }; 69 | 96F46E0827DFC57600F20CDC /* AlertService.AlertPackage.Button+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96F46E0727DFC57600F20CDC /* AlertService.AlertPackage.Button+Tests.swift */; }; 70 | E9959A7328666409006FDF5A /* AlertManager+Environment.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9959A7228666409006FDF5A /* AlertManager+Environment.swift */; }; 71 | /* End PBXBuildFile section */ 72 | 73 | /* Begin PBXContainerItemProxy section */ 74 | 96B4E07227432D6300EC88B3 /* PBXContainerItemProxy */ = { 75 | isa = PBXContainerItemProxy; 76 | containerPortal = 96B4E05927432D6100EC88B3 /* Project object */; 77 | proxyType = 1; 78 | remoteGlobalIDString = 96B4E06027432D6100EC88B3; 79 | remoteInfo = MVVM.Demo.SwiftUI; 80 | }; 81 | 96B4E07C27432D6300EC88B3 /* PBXContainerItemProxy */ = { 82 | isa = PBXContainerItemProxy; 83 | containerPortal = 96B4E05927432D6100EC88B3 /* Project object */; 84 | proxyType = 1; 85 | remoteGlobalIDString = 96B4E06027432D6100EC88B3; 86 | remoteInfo = MVVM.Demo.SwiftUI; 87 | }; 88 | /* End PBXContainerItemProxy section */ 89 | 90 | /* Begin PBXFileReference section */ 91 | 6FBF3B6729F10AA2008BB5B7 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = SOURCE_ROOT; }; 92 | 6FD86CD8293DE9060052C759 /* ObjectNavigationStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectNavigationStack.swift; sourceTree = ""; }; 93 | 6FDA2F102BE0573700A83DDB /* InfiniteCardsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteCardsView.swift; sourceTree = ""; }; 94 | 6FDA2F122BE0575000A83DDB /* InfiniteCardsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfiniteCardsViewModel.swift; sourceTree = ""; }; 95 | 6FF089B82BAC9387001C454E /* Resolver+Resolved.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Resolver+Resolved.swift"; sourceTree = ""; }; 96 | 9647788A2774F0EE002203DE /* AnyPublisher+AwaitResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPublisher+AwaitResults.swift"; sourceTree = ""; }; 97 | 9647788F2774F240002203DE /* AuthenticationService+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AuthenticationService+Tests.swift"; sourceTree = ""; }; 98 | 964778912774F64A002203DE /* Result+TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+TestExtensions.swift"; sourceTree = ""; }; 99 | 9665D9542745C1D60055F1F6 /* ColorWizardCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardCoordinatorView.swift; sourceTree = ""; }; 100 | 9665D9562745C1F10055F1F6 /* ColorWizardCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardCoordinator.swift; sourceTree = ""; }; 101 | 9665D95E2745C2E00055F1F6 /* ColorWizardContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardContentView.swift; sourceTree = ""; }; 102 | 9665D9602745C2FA0055F1F6 /* ColorWizardContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardContentViewModel.swift; sourceTree = ""; }; 103 | 9665D9622745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardConfigurationViewModel.swift; sourceTree = ""; }; 104 | 9665D9642745C3700055F1F6 /* ColorWizardConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorWizardConfiguration.swift; sourceTree = ""; }; 105 | 9665D9672745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColorWizardConfiguration+Mock.swift"; sourceTree = ""; }; 106 | 968D344C27CD941700340C18 /* AlertService.AlertPackage+View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertService.AlertPackage+View.swift"; sourceTree = ""; }; 107 | 968D344E27CD978600340C18 /* ColorService+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColorService+Tests.swift"; sourceTree = ""; }; 108 | 96B4E06127432D6100EC88B3 /* MVVM.Demo.SwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MVVM.Demo.SwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 109 | 96B4E06427432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVVM_Demo_SwiftUIApp.swift; sourceTree = ""; }; 110 | 96B4E06827432D6200EC88B3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 111 | 96B4E06B27432D6200EC88B3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 112 | 96B4E07127432D6300EC88B3 /* MVVM.Demo.SwiftUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MVVM.Demo.SwiftUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 113 | 96B4E07527432D6300EC88B3 /* MVVM_Demo_SwiftUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVVM_Demo_SwiftUITests.swift; sourceTree = ""; }; 114 | 96B4E07B27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = MVVM.Demo.SwiftUIUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 115 | 96B4E07F27432D6300EC88B3 /* MVVM_Demo_SwiftUIUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVVM_Demo_SwiftUIUITests.swift; sourceTree = ""; }; 116 | 96B4E08127432D6300EC88B3 /* MVVM_Demo_SwiftUIUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MVVM_Demo_SwiftUIUITestsLaunchTests.swift; sourceTree = ""; }; 117 | 96B4E09027432E1100EC88B3 /* ViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModel.swift; sourceTree = ""; }; 118 | 96B4E09227432E4000EC88B3 /* HapticFeedbackProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HapticFeedbackProvider.swift; sourceTree = ""; }; 119 | 96B4E09527432E6800EC88B3 /* CancelBag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancelBag.swift; sourceTree = ""; }; 120 | 96B4E09727432E7D00EC88B3 /* Just+Void.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Just+Void.swift"; sourceTree = ""; }; 121 | 96B4E09927432E9300EC88B3 /* Publisher+DefinedScheduler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+DefinedScheduler.swift"; sourceTree = ""; }; 122 | 96B4E09B27432EB100EC88B3 /* Publisher+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Publisher+Extensions.swift"; sourceTree = ""; }; 123 | 96B4E0A627432F1C00EC88B3 /* PassthroughSubject+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PassthroughSubject+Extensions.swift"; sourceTree = ""; }; 124 | 96B4E0AB27432F8900EC88B3 /* String+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Extensions.swift"; sourceTree = ""; }; 125 | 96B4E0AD27432FAB00EC88B3 /* UIApplication+EndEditing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+EndEditing.swift"; sourceTree = ""; }; 126 | 96B4E0AF27432FD000EC88B3 /* Button+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Button+Extensions.swift"; sourceTree = ""; }; 127 | 96B4E0B227432FEB00EC88B3 /* EdgeInsets+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "EdgeInsets+Extensions.swift"; sourceTree = ""; }; 128 | 96B4E0B82743303F00EC88B3 /* View+OnReceive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+OnReceive.swift"; sourceTree = ""; }; 129 | 96B4E0BD2743309B00EC88B3 /* AppAssembler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAssembler.swift; sourceTree = ""; }; 130 | 96B4E0BF274330B400EC88B3 /* CoordinatorAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorAssembly.swift; sourceTree = ""; }; 131 | 96B4E0C1274330CB00EC88B3 /* ServiceAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceAssembly.swift; sourceTree = ""; }; 132 | 96B4E0C3274330E600EC88B3 /* ViewModelAssembly.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewModelAssembly.swift; sourceTree = ""; }; 133 | 96B4E0C72743313900EC88B3 /* AppRootCoordinatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootCoordinatorView.swift; sourceTree = ""; }; 134 | 96B4E0C92743314700EC88B3 /* AppRootCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRootCoordinator.swift; sourceTree = ""; }; 135 | 96B4E0CC274331EE00EC88B3 /* AuthenticationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationService.swift; sourceTree = ""; }; 136 | 96B4E0CE274335A900EC88B3 /* ColorService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorService.swift; sourceTree = ""; }; 137 | 96B4E0D0274336EE00EC88B3 /* AlertService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertService.swift; sourceTree = ""; }; 138 | 96B4E0D32743382B00EC88B3 /* LandingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingView.swift; sourceTree = ""; }; 139 | 96B4E0D52743384F00EC88B3 /* LandingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LandingViewModel.swift; sourceTree = ""; }; 140 | 96B4E0D92743390E00EC88B3 /* BrightBorderedButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrightBorderedButtonStyle.swift; sourceTree = ""; }; 141 | 96B4E0DC2743393E00EC88B3 /* TextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextStyle.swift; sourceTree = ""; }; 142 | 96B4E0DE2743396600EC88B3 /* ButtonTextStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonTextStyle.swift; sourceTree = ""; }; 143 | 96B4E0E127437CA200EC88B3 /* SignInView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInView.swift; sourceTree = ""; }; 144 | 96B4E0E327437CB900EC88B3 /* SignInViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SignInViewModel.swift; sourceTree = ""; }; 145 | 96B4E0E627437D0600EC88B3 /* VisualEffectView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisualEffectView.swift; sourceTree = ""; }; 146 | 96B4E0E827437D5300EC88B3 /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; 147 | 96B4E0EA274386FA00EC88B3 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = ""; }; 148 | 96B4E0ED274424F600EC88B3 /* PulseView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulseView.swift; sourceTree = ""; }; 149 | 96B4E0EF274424FE00EC88B3 /* PulseViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PulseViewModel.swift; sourceTree = ""; }; 150 | 96F46E0527DFAF2700F20CDC /* AlertService+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertService+Tests.swift"; sourceTree = ""; }; 151 | 96F46E0727DFC57600F20CDC /* AlertService.AlertPackage.Button+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertService.AlertPackage.Button+Tests.swift"; sourceTree = ""; }; 152 | E9959A7228666409006FDF5A /* AlertManager+Environment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AlertManager+Environment.swift"; sourceTree = ""; }; 153 | /* End PBXFileReference section */ 154 | 155 | /* Begin PBXFrameworksBuildPhase section */ 156 | 96B4E05E27432D6100EC88B3 /* Frameworks */ = { 157 | isa = PBXFrameworksBuildPhase; 158 | buildActionMask = 2147483647; 159 | files = ( 160 | 96B4E0A227432EEE00EC88B3 /* CombineExt in Frameworks */, 161 | 96B4E0A527432EFC00EC88B3 /* Swinject in Frameworks */, 162 | 6FD86CDC2940033C0052C759 /* BusyIndicator in Frameworks */, 163 | ); 164 | runOnlyForDeploymentPostprocessing = 0; 165 | }; 166 | 96B4E06E27432D6300EC88B3 /* Frameworks */ = { 167 | isa = PBXFrameworksBuildPhase; 168 | buildActionMask = 2147483647; 169 | files = ( 170 | ); 171 | runOnlyForDeploymentPostprocessing = 0; 172 | }; 173 | 96B4E07827432D6300EC88B3 /* Frameworks */ = { 174 | isa = PBXFrameworksBuildPhase; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | ); 178 | runOnlyForDeploymentPostprocessing = 0; 179 | }; 180 | /* End PBXFrameworksBuildPhase section */ 181 | 182 | /* Begin PBXGroup section */ 183 | 6FDA2F0F2BE0572500A83DDB /* InfiniteCards */ = { 184 | isa = PBXGroup; 185 | children = ( 186 | 6FDA2F102BE0573700A83DDB /* InfiniteCardsView.swift */, 187 | 6FDA2F122BE0575000A83DDB /* InfiniteCardsViewModel.swift */, 188 | ); 189 | path = InfiniteCards; 190 | sourceTree = ""; 191 | }; 192 | 964778892774F0C9002203DE /* TestExtensions */ = { 193 | isa = PBXGroup; 194 | children = ( 195 | 9647788A2774F0EE002203DE /* AnyPublisher+AwaitResults.swift */, 196 | 964778912774F64A002203DE /* Result+TestExtensions.swift */, 197 | ); 198 | path = TestExtensions; 199 | sourceTree = ""; 200 | }; 201 | 9647788E2774F1FD002203DE /* Services */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 96F46E0427DFAEC900F20CDC /* AlertService */, 205 | 9647788F2774F240002203DE /* AuthenticationService+Tests.swift */, 206 | 968D344E27CD978600340C18 /* ColorService+Tests.swift */, 207 | ); 208 | path = Services; 209 | sourceTree = ""; 210 | }; 211 | 9665D9532745C1B50055F1F6 /* ColorWizard */ = { 212 | isa = PBXGroup; 213 | children = ( 214 | 9665D9692745CCB70055F1F6 /* Configuration */, 215 | 9665D95D2745C2C80055F1F6 /* Content */, 216 | 9665D9542745C1D60055F1F6 /* ColorWizardCoordinatorView.swift */, 217 | 9665D9562745C1F10055F1F6 /* ColorWizardCoordinator.swift */, 218 | ); 219 | path = ColorWizard; 220 | sourceTree = ""; 221 | }; 222 | 9665D95D2745C2C80055F1F6 /* Content */ = { 223 | isa = PBXGroup; 224 | children = ( 225 | 9665D95E2745C2E00055F1F6 /* ColorWizardContentView.swift */, 226 | 9665D9602745C2FA0055F1F6 /* ColorWizardContentViewModel.swift */, 227 | ); 228 | path = Content; 229 | sourceTree = ""; 230 | }; 231 | 9665D9692745CCB70055F1F6 /* Configuration */ = { 232 | isa = PBXGroup; 233 | children = ( 234 | 9665D9642745C3700055F1F6 /* ColorWizardConfiguration.swift */, 235 | 9665D9672745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift */, 236 | 9665D9622745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift */, 237 | ); 238 | path = Configuration; 239 | sourceTree = ""; 240 | }; 241 | 968D344B27CD93F500340C18 /* AlertService */ = { 242 | isa = PBXGroup; 243 | children = ( 244 | 96B4E0D0274336EE00EC88B3 /* AlertService.swift */, 245 | 968D344C27CD941700340C18 /* AlertService.AlertPackage+View.swift */, 246 | ); 247 | path = AlertService; 248 | sourceTree = ""; 249 | }; 250 | 96B4E05827432D6100EC88B3 = { 251 | isa = PBXGroup; 252 | children = ( 253 | 96B4E06327432D6100EC88B3 /* MVVM.Demo.SwiftUI */, 254 | 96B4E07427432D6300EC88B3 /* MVVM.Demo.SwiftUITests */, 255 | 96B4E07E27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests */, 256 | 96B4E06227432D6100EC88B3 /* Products */, 257 | ); 258 | indentWidth = 2; 259 | sourceTree = ""; 260 | tabWidth = 2; 261 | }; 262 | 96B4E06227432D6100EC88B3 /* Products */ = { 263 | isa = PBXGroup; 264 | children = ( 265 | 96B4E06127432D6100EC88B3 /* MVVM.Demo.SwiftUI.app */, 266 | 96B4E07127432D6300EC88B3 /* MVVM.Demo.SwiftUITests.xctest */, 267 | 96B4E07B27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests.xctest */, 268 | ); 269 | name = Products; 270 | sourceTree = ""; 271 | }; 272 | 96B4E06327432D6100EC88B3 /* MVVM.Demo.SwiftUI */ = { 273 | isa = PBXGroup; 274 | children = ( 275 | 6FBF3B6729F10AA2008BB5B7 /* README.md */, 276 | 96B4E0BC2743306A00EC88B3 /* Architecture */, 277 | 96B4E08F27432DEB00EC88B3 /* Core */, 278 | 96B4E0CB274331DB00EC88B3 /* Services */, 279 | 96B4E0C52743312600EC88B3 /* UI */, 280 | 96B4E06427432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift */, 281 | 96B4E06827432D6200EC88B3 /* Assets.xcassets */, 282 | 96B4E06A27432D6200EC88B3 /* Preview Content */, 283 | ); 284 | path = MVVM.Demo.SwiftUI; 285 | sourceTree = ""; 286 | }; 287 | 96B4E06A27432D6200EC88B3 /* Preview Content */ = { 288 | isa = PBXGroup; 289 | children = ( 290 | 96B4E06B27432D6200EC88B3 /* Preview Assets.xcassets */, 291 | ); 292 | path = "Preview Content"; 293 | sourceTree = ""; 294 | }; 295 | 96B4E07427432D6300EC88B3 /* MVVM.Demo.SwiftUITests */ = { 296 | isa = PBXGroup; 297 | children = ( 298 | 964778892774F0C9002203DE /* TestExtensions */, 299 | 9647788E2774F1FD002203DE /* Services */, 300 | 96B4E07527432D6300EC88B3 /* MVVM_Demo_SwiftUITests.swift */, 301 | ); 302 | path = MVVM.Demo.SwiftUITests; 303 | sourceTree = ""; 304 | }; 305 | 96B4E07E27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests */ = { 306 | isa = PBXGroup; 307 | children = ( 308 | 96B4E07F27432D6300EC88B3 /* MVVM_Demo_SwiftUIUITests.swift */, 309 | 96B4E08127432D6300EC88B3 /* MVVM_Demo_SwiftUIUITestsLaunchTests.swift */, 310 | ); 311 | path = MVVM.Demo.SwiftUIUITests; 312 | sourceTree = ""; 313 | }; 314 | 96B4E08F27432DEB00EC88B3 /* Core */ = { 315 | isa = PBXGroup; 316 | children = ( 317 | 96B4E09427432E5700EC88B3 /* Combine */, 318 | 96B4E0AA27432F7800EC88B3 /* Extensions */, 319 | 96B4E0B127432FD700EC88B3 /* SwiftUI */, 320 | 96B4E09227432E4000EC88B3 /* HapticFeedbackProvider.swift */, 321 | ); 322 | path = Core; 323 | sourceTree = ""; 324 | }; 325 | 96B4E09427432E5700EC88B3 /* Combine */ = { 326 | isa = PBXGroup; 327 | children = ( 328 | 96B4E09527432E6800EC88B3 /* CancelBag.swift */, 329 | 96B4E09727432E7D00EC88B3 /* Just+Void.swift */, 330 | 96B4E09927432E9300EC88B3 /* Publisher+DefinedScheduler.swift */, 331 | 96B4E09B27432EB100EC88B3 /* Publisher+Extensions.swift */, 332 | 96B4E0A627432F1C00EC88B3 /* PassthroughSubject+Extensions.swift */, 333 | ); 334 | path = Combine; 335 | sourceTree = ""; 336 | }; 337 | 96B4E0AA27432F7800EC88B3 /* Extensions */ = { 338 | isa = PBXGroup; 339 | children = ( 340 | 6FF089B82BAC9387001C454E /* Resolver+Resolved.swift */, 341 | 96B4E0AB27432F8900EC88B3 /* String+Extensions.swift */, 342 | 96B4E0AD27432FAB00EC88B3 /* UIApplication+EndEditing.swift */, 343 | ); 344 | path = Extensions; 345 | sourceTree = ""; 346 | }; 347 | 96B4E0B127432FD700EC88B3 /* SwiftUI */ = { 348 | isa = PBXGroup; 349 | children = ( 350 | 96B4E0AF27432FD000EC88B3 /* Button+Extensions.swift */, 351 | 96B4E0EA274386FA00EC88B3 /* Color+SystemColors.swift */, 352 | 96B4E0B227432FEB00EC88B3 /* EdgeInsets+Extensions.swift */, 353 | 6FD86CD8293DE9060052C759 /* ObjectNavigationStack.swift */, 354 | 96B4E0B82743303F00EC88B3 /* View+OnReceive.swift */, 355 | ); 356 | path = SwiftUI; 357 | sourceTree = ""; 358 | }; 359 | 96B4E0BC2743306A00EC88B3 /* Architecture */ = { 360 | isa = PBXGroup; 361 | children = ( 362 | 96B4E09027432E1100EC88B3 /* ViewModel.swift */, 363 | 96B4E0BD2743309B00EC88B3 /* AppAssembler.swift */, 364 | 96B4E0BF274330B400EC88B3 /* CoordinatorAssembly.swift */, 365 | 96B4E0C1274330CB00EC88B3 /* ServiceAssembly.swift */, 366 | 96B4E0C3274330E600EC88B3 /* ViewModelAssembly.swift */, 367 | ); 368 | path = Architecture; 369 | sourceTree = ""; 370 | }; 371 | 96B4E0C52743312600EC88B3 /* UI */ = { 372 | isa = PBXGroup; 373 | children = ( 374 | 96B4E0C62743312F00EC88B3 /* AppRootCoordinator */, 375 | 9665D9532745C1B50055F1F6 /* ColorWizard */, 376 | 6FDA2F0F2BE0572500A83DDB /* InfiniteCards */, 377 | 96B4E0D22743381500EC88B3 /* Landing */, 378 | 96B4E0EC274424E800EC88B3 /* Pulse */, 379 | 96B4E0E027437C9000EC88B3 /* SignIn */, 380 | 96B4E0D7274338D400EC88B3 /* Styles */, 381 | 96B4E0E527437CF700EC88B3 /* Views */, 382 | ); 383 | path = UI; 384 | sourceTree = ""; 385 | }; 386 | 96B4E0C62743312F00EC88B3 /* AppRootCoordinator */ = { 387 | isa = PBXGroup; 388 | children = ( 389 | 96B4E0C72743313900EC88B3 /* AppRootCoordinatorView.swift */, 390 | 96B4E0C92743314700EC88B3 /* AppRootCoordinator.swift */, 391 | ); 392 | path = AppRootCoordinator; 393 | sourceTree = ""; 394 | }; 395 | 96B4E0CB274331DB00EC88B3 /* Services */ = { 396 | isa = PBXGroup; 397 | children = ( 398 | 968D344B27CD93F500340C18 /* AlertService */, 399 | 96B4E0CC274331EE00EC88B3 /* AuthenticationService.swift */, 400 | 96B4E0CE274335A900EC88B3 /* ColorService.swift */, 401 | ); 402 | path = Services; 403 | sourceTree = ""; 404 | }; 405 | 96B4E0D22743381500EC88B3 /* Landing */ = { 406 | isa = PBXGroup; 407 | children = ( 408 | 96B4E0D32743382B00EC88B3 /* LandingView.swift */, 409 | 96B4E0D52743384F00EC88B3 /* LandingViewModel.swift */, 410 | ); 411 | path = Landing; 412 | sourceTree = ""; 413 | }; 414 | 96B4E0D7274338D400EC88B3 /* Styles */ = { 415 | isa = PBXGroup; 416 | children = ( 417 | 96B4E0D8274338E600EC88B3 /* ButtonStyles */, 418 | 96B4E0DB2743391E00EC88B3 /* TextStyles */, 419 | ); 420 | path = Styles; 421 | sourceTree = ""; 422 | }; 423 | 96B4E0D8274338E600EC88B3 /* ButtonStyles */ = { 424 | isa = PBXGroup; 425 | children = ( 426 | 96B4E0D92743390E00EC88B3 /* BrightBorderedButtonStyle.swift */, 427 | ); 428 | path = ButtonStyles; 429 | sourceTree = ""; 430 | }; 431 | 96B4E0DB2743391E00EC88B3 /* TextStyles */ = { 432 | isa = PBXGroup; 433 | children = ( 434 | 96B4E0DC2743393E00EC88B3 /* TextStyle.swift */, 435 | 96B4E0DE2743396600EC88B3 /* ButtonTextStyle.swift */, 436 | ); 437 | path = TextStyles; 438 | sourceTree = ""; 439 | }; 440 | 96B4E0E027437C9000EC88B3 /* SignIn */ = { 441 | isa = PBXGroup; 442 | children = ( 443 | 96B4E0E127437CA200EC88B3 /* SignInView.swift */, 444 | 96B4E0E327437CB900EC88B3 /* SignInViewModel.swift */, 445 | ); 446 | path = SignIn; 447 | sourceTree = ""; 448 | }; 449 | 96B4E0E527437CF700EC88B3 /* Views */ = { 450 | isa = PBXGroup; 451 | children = ( 452 | E9959A71286663F9006FDF5A /* Modifiers */, 453 | 96B4E0E827437D5300EC88B3 /* CardView.swift */, 454 | 96B4E0E627437D0600EC88B3 /* VisualEffectView.swift */, 455 | ); 456 | path = Views; 457 | sourceTree = ""; 458 | }; 459 | 96B4E0EC274424E800EC88B3 /* Pulse */ = { 460 | isa = PBXGroup; 461 | children = ( 462 | 96B4E0ED274424F600EC88B3 /* PulseView.swift */, 463 | 96B4E0EF274424FE00EC88B3 /* PulseViewModel.swift */, 464 | ); 465 | path = Pulse; 466 | sourceTree = ""; 467 | }; 468 | 96F46E0427DFAEC900F20CDC /* AlertService */ = { 469 | isa = PBXGroup; 470 | children = ( 471 | 96F46E0527DFAF2700F20CDC /* AlertService+Tests.swift */, 472 | 96F46E0727DFC57600F20CDC /* AlertService.AlertPackage.Button+Tests.swift */, 473 | ); 474 | path = AlertService; 475 | sourceTree = ""; 476 | }; 477 | E9959A71286663F9006FDF5A /* Modifiers */ = { 478 | isa = PBXGroup; 479 | children = ( 480 | E9959A7228666409006FDF5A /* AlertManager+Environment.swift */, 481 | ); 482 | path = Modifiers; 483 | sourceTree = ""; 484 | }; 485 | /* End PBXGroup section */ 486 | 487 | /* Begin PBXNativeTarget section */ 488 | 96B4E06027432D6100EC88B3 /* MVVM.Demo.SwiftUI */ = { 489 | isa = PBXNativeTarget; 490 | buildConfigurationList = 96B4E08527432D6300EC88B3 /* Build configuration list for PBXNativeTarget "MVVM.Demo.SwiftUI" */; 491 | buildPhases = ( 492 | 96B4E05D27432D6100EC88B3 /* Sources */, 493 | 96B4E05E27432D6100EC88B3 /* Frameworks */, 494 | 96B4E05F27432D6100EC88B3 /* Resources */, 495 | ); 496 | buildRules = ( 497 | ); 498 | dependencies = ( 499 | ); 500 | name = MVVM.Demo.SwiftUI; 501 | packageProductDependencies = ( 502 | 96B4E0A127432EEE00EC88B3 /* CombineExt */, 503 | 96B4E0A427432EFC00EC88B3 /* Swinject */, 504 | 6FD86CDB2940033C0052C759 /* BusyIndicator */, 505 | ); 506 | productName = MVVM.Demo.SwiftUI; 507 | productReference = 96B4E06127432D6100EC88B3 /* MVVM.Demo.SwiftUI.app */; 508 | productType = "com.apple.product-type.application"; 509 | }; 510 | 96B4E07027432D6300EC88B3 /* MVVM.Demo.SwiftUITests */ = { 511 | isa = PBXNativeTarget; 512 | buildConfigurationList = 96B4E08827432D6300EC88B3 /* Build configuration list for PBXNativeTarget "MVVM.Demo.SwiftUITests" */; 513 | buildPhases = ( 514 | 96B4E06D27432D6300EC88B3 /* Sources */, 515 | 96B4E06E27432D6300EC88B3 /* Frameworks */, 516 | 96B4E06F27432D6300EC88B3 /* Resources */, 517 | ); 518 | buildRules = ( 519 | ); 520 | dependencies = ( 521 | 96B4E07327432D6300EC88B3 /* PBXTargetDependency */, 522 | ); 523 | name = MVVM.Demo.SwiftUITests; 524 | productName = MVVM.Demo.SwiftUITests; 525 | productReference = 96B4E07127432D6300EC88B3 /* MVVM.Demo.SwiftUITests.xctest */; 526 | productType = "com.apple.product-type.bundle.unit-test"; 527 | }; 528 | 96B4E07A27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests */ = { 529 | isa = PBXNativeTarget; 530 | buildConfigurationList = 96B4E08B27432D6300EC88B3 /* Build configuration list for PBXNativeTarget "MVVM.Demo.SwiftUIUITests" */; 531 | buildPhases = ( 532 | 96B4E07727432D6300EC88B3 /* Sources */, 533 | 96B4E07827432D6300EC88B3 /* Frameworks */, 534 | 96B4E07927432D6300EC88B3 /* Resources */, 535 | ); 536 | buildRules = ( 537 | ); 538 | dependencies = ( 539 | 96B4E07D27432D6300EC88B3 /* PBXTargetDependency */, 540 | ); 541 | name = MVVM.Demo.SwiftUIUITests; 542 | productName = MVVM.Demo.SwiftUIUITests; 543 | productReference = 96B4E07B27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests.xctest */; 544 | productType = "com.apple.product-type.bundle.ui-testing"; 545 | }; 546 | /* End PBXNativeTarget section */ 547 | 548 | /* Begin PBXProject section */ 549 | 96B4E05927432D6100EC88B3 /* Project object */ = { 550 | isa = PBXProject; 551 | attributes = { 552 | BuildIndependentTargetsInParallel = 1; 553 | LastSwiftUpdateCheck = 1310; 554 | LastUpgradeCheck = 1310; 555 | TargetAttributes = { 556 | 96B4E06027432D6100EC88B3 = { 557 | CreatedOnToolsVersion = 13.1; 558 | }; 559 | 96B4E07027432D6300EC88B3 = { 560 | CreatedOnToolsVersion = 13.1; 561 | TestTargetID = 96B4E06027432D6100EC88B3; 562 | }; 563 | 96B4E07A27432D6300EC88B3 = { 564 | CreatedOnToolsVersion = 13.1; 565 | TestTargetID = 96B4E06027432D6100EC88B3; 566 | }; 567 | }; 568 | }; 569 | buildConfigurationList = 96B4E05C27432D6100EC88B3 /* Build configuration list for PBXProject "MVVM.Demo.SwiftUI" */; 570 | compatibilityVersion = "Xcode 13.0"; 571 | developmentRegion = en; 572 | hasScannedForEncodings = 0; 573 | knownRegions = ( 574 | en, 575 | Base, 576 | ); 577 | mainGroup = 96B4E05827432D6100EC88B3; 578 | packageReferences = ( 579 | 96B4E0A027432EEE00EC88B3 /* XCRemoteSwiftPackageReference "CombineExt" */, 580 | 96B4E0A327432EFC00EC88B3 /* XCRemoteSwiftPackageReference "Swinject" */, 581 | 6FD86CDA2940033C0052C759 /* XCRemoteSwiftPackageReference "BusyIndicator" */, 582 | ); 583 | productRefGroup = 96B4E06227432D6100EC88B3 /* Products */; 584 | projectDirPath = ""; 585 | projectRoot = ""; 586 | targets = ( 587 | 96B4E06027432D6100EC88B3 /* MVVM.Demo.SwiftUI */, 588 | 96B4E07027432D6300EC88B3 /* MVVM.Demo.SwiftUITests */, 589 | 96B4E07A27432D6300EC88B3 /* MVVM.Demo.SwiftUIUITests */, 590 | ); 591 | }; 592 | /* End PBXProject section */ 593 | 594 | /* Begin PBXResourcesBuildPhase section */ 595 | 96B4E05F27432D6100EC88B3 /* Resources */ = { 596 | isa = PBXResourcesBuildPhase; 597 | buildActionMask = 2147483647; 598 | files = ( 599 | 96B4E06C27432D6200EC88B3 /* Preview Assets.xcassets in Resources */, 600 | 96B4E06927432D6200EC88B3 /* Assets.xcassets in Resources */, 601 | ); 602 | runOnlyForDeploymentPostprocessing = 0; 603 | }; 604 | 96B4E06F27432D6300EC88B3 /* Resources */ = { 605 | isa = PBXResourcesBuildPhase; 606 | buildActionMask = 2147483647; 607 | files = ( 608 | ); 609 | runOnlyForDeploymentPostprocessing = 0; 610 | }; 611 | 96B4E07927432D6300EC88B3 /* Resources */ = { 612 | isa = PBXResourcesBuildPhase; 613 | buildActionMask = 2147483647; 614 | files = ( 615 | ); 616 | runOnlyForDeploymentPostprocessing = 0; 617 | }; 618 | /* End PBXResourcesBuildPhase section */ 619 | 620 | /* Begin PBXSourcesBuildPhase section */ 621 | 96B4E05D27432D6100EC88B3 /* Sources */ = { 622 | isa = PBXSourcesBuildPhase; 623 | buildActionMask = 2147483647; 624 | files = ( 625 | 9665D9682745C53B0055F1F6 /* ColorWizardConfiguration+Mock.swift in Sources */, 626 | 96B4E0E427437CBA00EC88B3 /* SignInViewModel.swift in Sources */, 627 | 96B4E0AC27432F8900EC88B3 /* String+Extensions.swift in Sources */, 628 | 96B4E0AE27432FAB00EC88B3 /* UIApplication+EndEditing.swift in Sources */, 629 | 96B4E0DA2743390E00EC88B3 /* BrightBorderedButtonStyle.swift in Sources */, 630 | 96B4E0EB274386FA00EC88B3 /* Color+SystemColors.swift in Sources */, 631 | 9665D95F2745C2E00055F1F6 /* ColorWizardContentView.swift in Sources */, 632 | 9665D9632745C3410055F1F6 /* ColorWizardConfigurationViewModel.swift in Sources */, 633 | 96B4E0C4274330E600EC88B3 /* ViewModelAssembly.swift in Sources */, 634 | 96B4E09327432E4000EC88B3 /* HapticFeedbackProvider.swift in Sources */, 635 | 96B4E0C82743313900EC88B3 /* AppRootCoordinatorView.swift in Sources */, 636 | 96B4E09627432E6800EC88B3 /* CancelBag.swift in Sources */, 637 | 96B4E0D42743382B00EC88B3 /* LandingView.swift in Sources */, 638 | 96B4E0CF274335A900EC88B3 /* ColorService.swift in Sources */, 639 | 96B4E0CA2743314700EC88B3 /* AppRootCoordinator.swift in Sources */, 640 | 96B4E0B92743303F00EC88B3 /* View+OnReceive.swift in Sources */, 641 | 96B4E0D62743384F00EC88B3 /* LandingViewModel.swift in Sources */, 642 | 96B4E0E727437D0600EC88B3 /* VisualEffectView.swift in Sources */, 643 | 6FF089B92BAC9387001C454E /* Resolver+Resolved.swift in Sources */, 644 | 96B4E0C0274330B400EC88B3 /* CoordinatorAssembly.swift in Sources */, 645 | 96B4E0B027432FD000EC88B3 /* Button+Extensions.swift in Sources */, 646 | 96B4E09127432E1100EC88B3 /* ViewModel.swift in Sources */, 647 | 96B4E0C2274330CB00EC88B3 /* ServiceAssembly.swift in Sources */, 648 | 96B4E06527432D6100EC88B3 /* MVVM_Demo_SwiftUIApp.swift in Sources */, 649 | 6FDA2F132BE0575000A83DDB /* InfiniteCardsViewModel.swift in Sources */, 650 | 96B4E0A727432F1C00EC88B3 /* PassthroughSubject+Extensions.swift in Sources */, 651 | 96B4E09827432E7D00EC88B3 /* Just+Void.swift in Sources */, 652 | 9665D9552745C1D60055F1F6 /* ColorWizardCoordinatorView.swift in Sources */, 653 | 968D344D27CD941700340C18 /* AlertService.AlertPackage+View.swift in Sources */, 654 | 9665D9612745C2FA0055F1F6 /* ColorWizardContentViewModel.swift in Sources */, 655 | 96B4E0D1274336EE00EC88B3 /* AlertService.swift in Sources */, 656 | 96B4E0BE2743309B00EC88B3 /* AppAssembler.swift in Sources */, 657 | 96B4E09C27432EB100EC88B3 /* Publisher+Extensions.swift in Sources */, 658 | 6FDA2F112BE0573700A83DDB /* InfiniteCardsView.swift in Sources */, 659 | 96B4E0F0274424FE00EC88B3 /* PulseViewModel.swift in Sources */, 660 | 96B4E0E227437CA200EC88B3 /* SignInView.swift in Sources */, 661 | 96B4E0DF2743396600EC88B3 /* ButtonTextStyle.swift in Sources */, 662 | 96B4E0DD2743393E00EC88B3 /* TextStyle.swift in Sources */, 663 | E9959A7328666409006FDF5A /* AlertManager+Environment.swift in Sources */, 664 | 96B4E0B327432FEB00EC88B3 /* EdgeInsets+Extensions.swift in Sources */, 665 | 96B4E09A27432E9300EC88B3 /* Publisher+DefinedScheduler.swift in Sources */, 666 | 96B4E0EE274424F600EC88B3 /* PulseView.swift in Sources */, 667 | 96B4E0E927437D5300EC88B3 /* CardView.swift in Sources */, 668 | 6FD86CD9293DE9060052C759 /* ObjectNavigationStack.swift in Sources */, 669 | 9665D9652745C3700055F1F6 /* ColorWizardConfiguration.swift in Sources */, 670 | 96B4E0CD274331EE00EC88B3 /* AuthenticationService.swift in Sources */, 671 | 9665D9572745C1F10055F1F6 /* ColorWizardCoordinator.swift in Sources */, 672 | ); 673 | runOnlyForDeploymentPostprocessing = 0; 674 | }; 675 | 96B4E06D27432D6300EC88B3 /* Sources */ = { 676 | isa = PBXSourcesBuildPhase; 677 | buildActionMask = 2147483647; 678 | files = ( 679 | 96B4E07627432D6300EC88B3 /* MVVM_Demo_SwiftUITests.swift in Sources */, 680 | 9647788B2774F0EE002203DE /* AnyPublisher+AwaitResults.swift in Sources */, 681 | 964778902774F240002203DE /* AuthenticationService+Tests.swift in Sources */, 682 | 968D344F27CD978600340C18 /* ColorService+Tests.swift in Sources */, 683 | 96F46E0827DFC57600F20CDC /* AlertService.AlertPackage.Button+Tests.swift in Sources */, 684 | 96F46E0627DFAF2700F20CDC /* AlertService+Tests.swift in Sources */, 685 | 964778922774F64A002203DE /* Result+TestExtensions.swift in Sources */, 686 | ); 687 | runOnlyForDeploymentPostprocessing = 0; 688 | }; 689 | 96B4E07727432D6300EC88B3 /* Sources */ = { 690 | isa = PBXSourcesBuildPhase; 691 | buildActionMask = 2147483647; 692 | files = ( 693 | 96B4E08027432D6300EC88B3 /* MVVM_Demo_SwiftUIUITests.swift in Sources */, 694 | 96B4E08227432D6300EC88B3 /* MVVM_Demo_SwiftUIUITestsLaunchTests.swift in Sources */, 695 | ); 696 | runOnlyForDeploymentPostprocessing = 0; 697 | }; 698 | /* End PBXSourcesBuildPhase section */ 699 | 700 | /* Begin PBXTargetDependency section */ 701 | 96B4E07327432D6300EC88B3 /* PBXTargetDependency */ = { 702 | isa = PBXTargetDependency; 703 | target = 96B4E06027432D6100EC88B3 /* MVVM.Demo.SwiftUI */; 704 | targetProxy = 96B4E07227432D6300EC88B3 /* PBXContainerItemProxy */; 705 | }; 706 | 96B4E07D27432D6300EC88B3 /* PBXTargetDependency */ = { 707 | isa = PBXTargetDependency; 708 | target = 96B4E06027432D6100EC88B3 /* MVVM.Demo.SwiftUI */; 709 | targetProxy = 96B4E07C27432D6300EC88B3 /* PBXContainerItemProxy */; 710 | }; 711 | /* End PBXTargetDependency section */ 712 | 713 | /* Begin XCBuildConfiguration section */ 714 | 96B4E08327432D6300EC88B3 /* Debug */ = { 715 | isa = XCBuildConfiguration; 716 | buildSettings = { 717 | ALWAYS_SEARCH_USER_PATHS = NO; 718 | CLANG_ANALYZER_NONNULL = YES; 719 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 720 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 721 | CLANG_CXX_LIBRARY = "libc++"; 722 | CLANG_ENABLE_MODULES = YES; 723 | CLANG_ENABLE_OBJC_ARC = YES; 724 | CLANG_ENABLE_OBJC_WEAK = YES; 725 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 726 | CLANG_WARN_BOOL_CONVERSION = YES; 727 | CLANG_WARN_COMMA = YES; 728 | CLANG_WARN_CONSTANT_CONVERSION = YES; 729 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 730 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 731 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 732 | CLANG_WARN_EMPTY_BODY = YES; 733 | CLANG_WARN_ENUM_CONVERSION = YES; 734 | CLANG_WARN_INFINITE_RECURSION = YES; 735 | CLANG_WARN_INT_CONVERSION = YES; 736 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 737 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 738 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 739 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 740 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 741 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 742 | CLANG_WARN_STRICT_PROTOTYPES = YES; 743 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 744 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 745 | CLANG_WARN_UNREACHABLE_CODE = YES; 746 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 747 | COPY_PHASE_STRIP = NO; 748 | DEBUG_INFORMATION_FORMAT = dwarf; 749 | ENABLE_STRICT_OBJC_MSGSEND = YES; 750 | ENABLE_TESTABILITY = YES; 751 | GCC_C_LANGUAGE_STANDARD = gnu11; 752 | GCC_DYNAMIC_NO_PIC = NO; 753 | GCC_NO_COMMON_BLOCKS = YES; 754 | GCC_OPTIMIZATION_LEVEL = 0; 755 | GCC_PREPROCESSOR_DEFINITIONS = ( 756 | "DEBUG=1", 757 | "$(inherited)", 758 | ); 759 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 760 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 761 | GCC_WARN_UNDECLARED_SELECTOR = YES; 762 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 763 | GCC_WARN_UNUSED_FUNCTION = YES; 764 | GCC_WARN_UNUSED_VARIABLE = YES; 765 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 766 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 767 | MTL_FAST_MATH = YES; 768 | ONLY_ACTIVE_ARCH = YES; 769 | SDKROOT = iphoneos; 770 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 771 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 772 | }; 773 | name = Debug; 774 | }; 775 | 96B4E08427432D6300EC88B3 /* Release */ = { 776 | isa = XCBuildConfiguration; 777 | buildSettings = { 778 | ALWAYS_SEARCH_USER_PATHS = NO; 779 | CLANG_ANALYZER_NONNULL = YES; 780 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 781 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; 782 | CLANG_CXX_LIBRARY = "libc++"; 783 | CLANG_ENABLE_MODULES = YES; 784 | CLANG_ENABLE_OBJC_ARC = YES; 785 | CLANG_ENABLE_OBJC_WEAK = YES; 786 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 787 | CLANG_WARN_BOOL_CONVERSION = YES; 788 | CLANG_WARN_COMMA = YES; 789 | CLANG_WARN_CONSTANT_CONVERSION = YES; 790 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 791 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 792 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 793 | CLANG_WARN_EMPTY_BODY = YES; 794 | CLANG_WARN_ENUM_CONVERSION = YES; 795 | CLANG_WARN_INFINITE_RECURSION = YES; 796 | CLANG_WARN_INT_CONVERSION = YES; 797 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 798 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 799 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 800 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 801 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 802 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 803 | CLANG_WARN_STRICT_PROTOTYPES = YES; 804 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 805 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 806 | CLANG_WARN_UNREACHABLE_CODE = YES; 807 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 808 | COPY_PHASE_STRIP = NO; 809 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 810 | ENABLE_NS_ASSERTIONS = NO; 811 | ENABLE_STRICT_OBJC_MSGSEND = YES; 812 | GCC_C_LANGUAGE_STANDARD = gnu11; 813 | GCC_NO_COMMON_BLOCKS = YES; 814 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 815 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 816 | GCC_WARN_UNDECLARED_SELECTOR = YES; 817 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 818 | GCC_WARN_UNUSED_FUNCTION = YES; 819 | GCC_WARN_UNUSED_VARIABLE = YES; 820 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 821 | MTL_ENABLE_DEBUG_INFO = NO; 822 | MTL_FAST_MATH = YES; 823 | SDKROOT = iphoneos; 824 | SWIFT_COMPILATION_MODE = wholemodule; 825 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 826 | VALIDATE_PRODUCT = YES; 827 | }; 828 | name = Release; 829 | }; 830 | 96B4E08627432D6300EC88B3 /* Debug */ = { 831 | isa = XCBuildConfiguration; 832 | buildSettings = { 833 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 834 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 835 | CODE_SIGN_STYLE = Automatic; 836 | CURRENT_PROJECT_VERSION = 1; 837 | DEVELOPMENT_ASSET_PATHS = "\"MVVM.Demo.SwiftUI/Preview Content\""; 838 | DEVELOPMENT_TEAM = QGV9TK3SFM; 839 | ENABLE_PREVIEWS = YES; 840 | GENERATE_INFOPLIST_FILE = YES; 841 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 842 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 843 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 844 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 845 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 846 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 847 | LD_RUNPATH_SEARCH_PATHS = ( 848 | "$(inherited)", 849 | "@executable_path/Frameworks", 850 | ); 851 | MARKETING_VERSION = 3.0.0; 852 | PRODUCT_BUNDLE_IDENTIFIER = "com.jasonrapai.MVVM-Demo-SwiftUI"; 853 | PRODUCT_NAME = "$(TARGET_NAME)"; 854 | SWIFT_EMIT_LOC_STRINGS = YES; 855 | SWIFT_VERSION = 5.0; 856 | TARGETED_DEVICE_FAMILY = "1,2"; 857 | }; 858 | name = Debug; 859 | }; 860 | 96B4E08727432D6300EC88B3 /* Release */ = { 861 | isa = XCBuildConfiguration; 862 | buildSettings = { 863 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 864 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 865 | CODE_SIGN_STYLE = Automatic; 866 | CURRENT_PROJECT_VERSION = 1; 867 | DEVELOPMENT_ASSET_PATHS = "\"MVVM.Demo.SwiftUI/Preview Content\""; 868 | DEVELOPMENT_TEAM = QGV9TK3SFM; 869 | ENABLE_PREVIEWS = YES; 870 | GENERATE_INFOPLIST_FILE = YES; 871 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 872 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 873 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 874 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 875 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 876 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 877 | LD_RUNPATH_SEARCH_PATHS = ( 878 | "$(inherited)", 879 | "@executable_path/Frameworks", 880 | ); 881 | MARKETING_VERSION = 3.0.0; 882 | PRODUCT_BUNDLE_IDENTIFIER = "com.jasonrapai.MVVM-Demo-SwiftUI"; 883 | PRODUCT_NAME = "$(TARGET_NAME)"; 884 | SWIFT_EMIT_LOC_STRINGS = YES; 885 | SWIFT_VERSION = 5.0; 886 | TARGETED_DEVICE_FAMILY = "1,2"; 887 | }; 888 | name = Release; 889 | }; 890 | 96B4E08927432D6300EC88B3 /* Debug */ = { 891 | isa = XCBuildConfiguration; 892 | buildSettings = { 893 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 894 | BUNDLE_LOADER = "$(TEST_HOST)"; 895 | CODE_SIGN_STYLE = Automatic; 896 | CURRENT_PROJECT_VERSION = 1; 897 | DEVELOPMENT_TEAM = QGV9TK3SFM; 898 | GENERATE_INFOPLIST_FILE = YES; 899 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 900 | LD_RUNPATH_SEARCH_PATHS = ( 901 | "$(inherited)", 902 | "@executable_path/Frameworks", 903 | "@loader_path/Frameworks", 904 | ); 905 | MARKETING_VERSION = 1.0; 906 | PRODUCT_BUNDLE_IDENTIFIER = "com.jasonrapai.MVVM-Demo-SwiftUITests"; 907 | PRODUCT_NAME = "$(TARGET_NAME)"; 908 | SWIFT_EMIT_LOC_STRINGS = NO; 909 | SWIFT_VERSION = 5.0; 910 | TARGETED_DEVICE_FAMILY = "1,2"; 911 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MVVM.Demo.SwiftUI.app/MVVM.Demo.SwiftUI"; 912 | }; 913 | name = Debug; 914 | }; 915 | 96B4E08A27432D6300EC88B3 /* Release */ = { 916 | isa = XCBuildConfiguration; 917 | buildSettings = { 918 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 919 | BUNDLE_LOADER = "$(TEST_HOST)"; 920 | CODE_SIGN_STYLE = Automatic; 921 | CURRENT_PROJECT_VERSION = 1; 922 | DEVELOPMENT_TEAM = QGV9TK3SFM; 923 | GENERATE_INFOPLIST_FILE = YES; 924 | IPHONEOS_DEPLOYMENT_TARGET = 17.0; 925 | LD_RUNPATH_SEARCH_PATHS = ( 926 | "$(inherited)", 927 | "@executable_path/Frameworks", 928 | "@loader_path/Frameworks", 929 | ); 930 | MARKETING_VERSION = 1.0; 931 | PRODUCT_BUNDLE_IDENTIFIER = "com.jasonrapai.MVVM-Demo-SwiftUITests"; 932 | PRODUCT_NAME = "$(TARGET_NAME)"; 933 | SWIFT_EMIT_LOC_STRINGS = NO; 934 | SWIFT_VERSION = 5.0; 935 | TARGETED_DEVICE_FAMILY = "1,2"; 936 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/MVVM.Demo.SwiftUI.app/MVVM.Demo.SwiftUI"; 937 | }; 938 | name = Release; 939 | }; 940 | 96B4E08C27432D6300EC88B3 /* Debug */ = { 941 | isa = XCBuildConfiguration; 942 | buildSettings = { 943 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 944 | CODE_SIGN_STYLE = Automatic; 945 | CURRENT_PROJECT_VERSION = 1; 946 | DEVELOPMENT_TEAM = QGV9TK3SFM; 947 | GENERATE_INFOPLIST_FILE = YES; 948 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 949 | LD_RUNPATH_SEARCH_PATHS = ( 950 | "$(inherited)", 951 | "@executable_path/Frameworks", 952 | "@loader_path/Frameworks", 953 | ); 954 | MARKETING_VERSION = 1.0; 955 | PRODUCT_BUNDLE_IDENTIFIER = "com.jasonrapai.MVVM-Demo-SwiftUIUITests"; 956 | PRODUCT_NAME = "$(TARGET_NAME)"; 957 | SWIFT_EMIT_LOC_STRINGS = NO; 958 | SWIFT_VERSION = 5.0; 959 | TARGETED_DEVICE_FAMILY = "1,2"; 960 | TEST_TARGET_NAME = MVVM.Demo.SwiftUI; 961 | }; 962 | name = Debug; 963 | }; 964 | 96B4E08D27432D6300EC88B3 /* Release */ = { 965 | isa = XCBuildConfiguration; 966 | buildSettings = { 967 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; 968 | CODE_SIGN_STYLE = Automatic; 969 | CURRENT_PROJECT_VERSION = 1; 970 | DEVELOPMENT_TEAM = QGV9TK3SFM; 971 | GENERATE_INFOPLIST_FILE = YES; 972 | IPHONEOS_DEPLOYMENT_TARGET = 16.1; 973 | LD_RUNPATH_SEARCH_PATHS = ( 974 | "$(inherited)", 975 | "@executable_path/Frameworks", 976 | "@loader_path/Frameworks", 977 | ); 978 | MARKETING_VERSION = 1.0; 979 | PRODUCT_BUNDLE_IDENTIFIER = "com.jasonrapai.MVVM-Demo-SwiftUIUITests"; 980 | PRODUCT_NAME = "$(TARGET_NAME)"; 981 | SWIFT_EMIT_LOC_STRINGS = NO; 982 | SWIFT_VERSION = 5.0; 983 | TARGETED_DEVICE_FAMILY = "1,2"; 984 | TEST_TARGET_NAME = MVVM.Demo.SwiftUI; 985 | }; 986 | name = Release; 987 | }; 988 | /* End XCBuildConfiguration section */ 989 | 990 | /* Begin XCConfigurationList section */ 991 | 96B4E05C27432D6100EC88B3 /* Build configuration list for PBXProject "MVVM.Demo.SwiftUI" */ = { 992 | isa = XCConfigurationList; 993 | buildConfigurations = ( 994 | 96B4E08327432D6300EC88B3 /* Debug */, 995 | 96B4E08427432D6300EC88B3 /* Release */, 996 | ); 997 | defaultConfigurationIsVisible = 0; 998 | defaultConfigurationName = Release; 999 | }; 1000 | 96B4E08527432D6300EC88B3 /* Build configuration list for PBXNativeTarget "MVVM.Demo.SwiftUI" */ = { 1001 | isa = XCConfigurationList; 1002 | buildConfigurations = ( 1003 | 96B4E08627432D6300EC88B3 /* Debug */, 1004 | 96B4E08727432D6300EC88B3 /* Release */, 1005 | ); 1006 | defaultConfigurationIsVisible = 0; 1007 | defaultConfigurationName = Release; 1008 | }; 1009 | 96B4E08827432D6300EC88B3 /* Build configuration list for PBXNativeTarget "MVVM.Demo.SwiftUITests" */ = { 1010 | isa = XCConfigurationList; 1011 | buildConfigurations = ( 1012 | 96B4E08927432D6300EC88B3 /* Debug */, 1013 | 96B4E08A27432D6300EC88B3 /* Release */, 1014 | ); 1015 | defaultConfigurationIsVisible = 0; 1016 | defaultConfigurationName = Release; 1017 | }; 1018 | 96B4E08B27432D6300EC88B3 /* Build configuration list for PBXNativeTarget "MVVM.Demo.SwiftUIUITests" */ = { 1019 | isa = XCConfigurationList; 1020 | buildConfigurations = ( 1021 | 96B4E08C27432D6300EC88B3 /* Debug */, 1022 | 96B4E08D27432D6300EC88B3 /* Release */, 1023 | ); 1024 | defaultConfigurationIsVisible = 0; 1025 | defaultConfigurationName = Release; 1026 | }; 1027 | /* End XCConfigurationList section */ 1028 | 1029 | /* Begin XCRemoteSwiftPackageReference section */ 1030 | 6FD86CDA2940033C0052C759 /* XCRemoteSwiftPackageReference "BusyIndicator" */ = { 1031 | isa = XCRemoteSwiftPackageReference; 1032 | repositoryURL = "git@github.com:jasonjrr/BusyIndicator.git"; 1033 | requirement = { 1034 | kind = exactVersion; 1035 | version = 1.1.0; 1036 | }; 1037 | }; 1038 | 96B4E0A027432EEE00EC88B3 /* XCRemoteSwiftPackageReference "CombineExt" */ = { 1039 | isa = XCRemoteSwiftPackageReference; 1040 | repositoryURL = "https://github.com/CombineCommunity/CombineExt"; 1041 | requirement = { 1042 | kind = exactVersion; 1043 | version = 1.6.1; 1044 | }; 1045 | }; 1046 | 96B4E0A327432EFC00EC88B3 /* XCRemoteSwiftPackageReference "Swinject" */ = { 1047 | isa = XCRemoteSwiftPackageReference; 1048 | repositoryURL = "https://github.com/Swinject/Swinject"; 1049 | requirement = { 1050 | kind = exactVersion; 1051 | version = 2.8.1; 1052 | }; 1053 | }; 1054 | /* End XCRemoteSwiftPackageReference section */ 1055 | 1056 | /* Begin XCSwiftPackageProductDependency section */ 1057 | 6FD86CDB2940033C0052C759 /* BusyIndicator */ = { 1058 | isa = XCSwiftPackageProductDependency; 1059 | package = 6FD86CDA2940033C0052C759 /* XCRemoteSwiftPackageReference "BusyIndicator" */; 1060 | productName = BusyIndicator; 1061 | }; 1062 | 96B4E0A127432EEE00EC88B3 /* CombineExt */ = { 1063 | isa = XCSwiftPackageProductDependency; 1064 | package = 96B4E0A027432EEE00EC88B3 /* XCRemoteSwiftPackageReference "CombineExt" */; 1065 | productName = CombineExt; 1066 | }; 1067 | 96B4E0A427432EFC00EC88B3 /* Swinject */ = { 1068 | isa = XCSwiftPackageProductDependency; 1069 | package = 96B4E0A327432EFC00EC88B3 /* XCRemoteSwiftPackageReference "Swinject" */; 1070 | productName = Swinject; 1071 | }; 1072 | /* End XCSwiftPackageProductDependency section */ 1073 | }; 1074 | rootObject = 96B4E05927432D6100EC88B3 /* Project object */; 1075 | } 1076 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "27ef233dac445591899bc4493120379c64da9f3e5d682bb83c658828ff38ec5e", 3 | "pins" : [ 4 | { 5 | "identity" : "busyindicator", 6 | "kind" : "remoteSourceControl", 7 | "location" : "git@github.com:jasonjrr/BusyIndicator.git", 8 | "state" : { 9 | "revision" : "6c055630b41f7f0d1faaf67ad7a991e43460bbb6", 10 | "version" : "1.1.0" 11 | } 12 | }, 13 | { 14 | "identity" : "combineext", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/CombineCommunity/CombineExt", 17 | "state" : { 18 | "revision" : "38a4d4cb01f8c7750671c786d33dfbea00cbd131", 19 | "version" : "1.6.1" 20 | } 21 | }, 22 | { 23 | "identity" : "swinject", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/Swinject/Swinject", 26 | "state" : { 27 | "revision" : "f10b6e9ebff440f985c43008f7c2d097639fcb81", 28 | "version" : "2.8.1" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI.xcodeproj/xcshareddata/xcschemes/MVVM.Demo.SwiftUI.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 39 | 40 | 41 | 42 | 44 | 50 | 51 | 52 | 54 | 60 | 61 | 62 | 63 | 64 | 74 | 76 | 82 | 83 | 84 | 85 | 91 | 93 | 99 | 100 | 101 | 102 | 104 | 105 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Architecture/AppAssembler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppAssembler.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Swinject 10 | 11 | class AppAssembler { 12 | private let assembler: Assembler 13 | 14 | var resolver: Resolver { self.assembler.resolver } 15 | 16 | init() { 17 | self.assembler = Assembler([ 18 | CoordinatorAssembly(), 19 | ServiceAssembly(), 20 | ViewModelAssembly(), 21 | ]) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Architecture/CoordinatorAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CoordinatorAssembly.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Swinject 10 | 11 | class CoordinatorAssembly: Assembly { 12 | func assemble(container: Container) { 13 | container.register(AppRootCoordinator.self) { r in 14 | AppRootCoordinator(resolver: r) 15 | }.inObjectScope(.container) 16 | 17 | container.register(ColorWizardCoordinator.self) { r in 18 | ColorWizardCoordinator(resolver: r) 19 | }.inObjectScope(.transient) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Architecture/ServiceAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ServiceAssembly.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Swinject 10 | import BusyIndicator 11 | 12 | class ServiceAssembly: Assembly { 13 | func assemble(container: Container) { 14 | container.register(AlertManager.self) { r in 15 | AlertManager() 16 | }.inObjectScope(.container) 17 | 18 | container.register(AlertServiceProtocol.self) { r in 19 | AlertService(alertManager: r.resolved(AlertManager.self)) 20 | }.inObjectScope(.container) 21 | 22 | container.register(AuthenticationServiceProtocol.self) { r in 23 | AuthenticationService( 24 | busyIndicatorService: r.resolved(BusyIndicatorServiceProtocol.self)) 25 | }.inObjectScope(.container) 26 | 27 | container.register(ColorServiceProtocol.self) { r in 28 | ColorService() 29 | }.inObjectScope(.container) 30 | 31 | assembleThirdParties(container: container) 32 | } 33 | 34 | func assembleThirdParties(container: Container) { 35 | container.register(BusyIndicatorServiceProtocol.self) { _ in 36 | let config = BusyIndicatorConfiguration() 37 | config.showBusyIndicatorDelay = 0 38 | return BusyIndicatorService(configuration: config) 39 | }.inObjectScope(.container) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Architecture/ViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | 10 | typealias ViewModelDefinition = (AnyObject & Identifiable & Hashable & HapticFeedbackProvider) 11 | 12 | protocol ViewModel: ViewModelDefinition {} 13 | 14 | extension ViewModel { 15 | static func ==(lhs: Self, rhs: Self) -> Bool { 16 | lhs === rhs 17 | } 18 | 19 | func hash(into hasher: inout Hasher) { 20 | hasher.combine(self.id) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Architecture/ViewModelAssembly.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewModelAssembly.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Swinject 10 | 11 | class ViewModelAssembly: Assembly { 12 | func assemble(container: Container) { 13 | container.register(ColorWizardContentViewModel.self) { r in 14 | ColorWizardContentViewModel() 15 | }.inObjectScope(.transient) 16 | 17 | container.register(InfiniteCardsViewModel.self) { r in 18 | InfiniteCardsViewModel( 19 | colorService: r.resolved(ColorServiceProtocol.self)) 20 | }.inObjectScope(.transient) 21 | 22 | container.register(LandingViewModel.self) { r in 23 | LandingViewModel( 24 | alertService: r.resolved(AlertServiceProtocol.self), 25 | authenticationService: r.resolved(AuthenticationServiceProtocol.self), 26 | colorService: r.resolved(ColorServiceProtocol.self)) 27 | }.inObjectScope(.transient) 28 | 29 | container.register(PulseViewModel.self) { r in 30 | PulseViewModel( 31 | authenticationService: r.resolved(AuthenticationServiceProtocol.self), 32 | colorService: r.resolved(ColorServiceProtocol.self)) 33 | }.inObjectScope(.transient) 34 | 35 | container.register(SignInViewModel.self) { r in 36 | SignInViewModel( 37 | authenticationService: r.resolved(AuthenticationServiceProtocol.self)) 38 | }.inObjectScope(.transient) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /MVVM.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 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Combine/CancelBag.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CancelBag.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Combine 9 | 10 | typealias CancelBag = Set 11 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Combine/Just+Void.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Just+Void.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Combine 9 | 10 | extension Just where Output == Void { 11 | init() { 12 | self.init(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Combine/PassthroughSubject+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PassthroughSubject+Extensions.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Combine 9 | 10 | extension PassthroughSubject where Output == Void { 11 | func send() { 12 | self.send(()) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Combine/Publisher+DefinedScheduler.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+DefinedScheduler.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | enum DefinedScheduler { 12 | case global(qos: DispatchQoS.QoSClass) 13 | case main 14 | case named(String) 15 | 16 | var dispatchQueue: DispatchQueue { 17 | switch self { 18 | case .global(let qos): return DispatchQueue.global(qos: qos) 19 | case .main: return DispatchQueue.main 20 | case .named(let name): return DispatchQueue(label: name) 21 | } 22 | } 23 | } 24 | 25 | extension Publisher { 26 | /// downstream 27 | func receive(on definedScheduler: DefinedScheduler) -> Publishers.ReceiveOn { 28 | self.receive(on: definedScheduler.dispatchQueue) 29 | } 30 | 31 | /// upstream 32 | func subscribe(on definedScheduler: DefinedScheduler) -> Publishers.SubscribeOn { 33 | self.subscribe(on: definedScheduler.dispatchQueue) 34 | } 35 | 36 | func debounce(for dueTime: DispatchQueue.SchedulerTimeType.Stride, on definedScheduler: DefinedScheduler) -> Publishers.Debounce { 37 | self.debounce(for: dueTime, scheduler: definedScheduler.dispatchQueue) 38 | } 39 | 40 | func throttle(for dueTime: DispatchQueue.SchedulerTimeType.Stride, on definedScheduler: DefinedScheduler, latest: Bool) -> Publishers.Throttle { 41 | self.throttle(for: dueTime, scheduler: definedScheduler.dispatchQueue, latest: latest) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Combine/Publisher+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Publisher+Extensions.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Combine 9 | import CombineExt 10 | 11 | extension Publisher { 12 | /// Republishes elements received from a publisher, by assigning them to a property marked as a publisher, but does not hold a strong reference to the assigned property. 13 | /// 14 | /// Use this operator when you want to receive elements from a publisher and republish them through a property marked with the `@Published` attribute. The `assign(to:)` operator manages the life cycle of the subscription, canceling the subscription automatically when the ``Published`` instance deinitializes. Because of this, the `assign(to:)` operator doesn't return an ``AnyCancellable`` that you're responsible for like ``assign(to:on:)`` does. 15 | /// 16 | /// The example below shows a model class that receives elements from an internal , and assigns them to a `@Published` property called `lastUpdated`. Because the `to` parameter has the `inout` keyword, you need to use the `&` operator when calling this method. 17 | /// 18 | /// class MyModel: ObservableObject { 19 | /// @Published var lastUpdated: Date = Date() 20 | /// init() { 21 | /// Timer.publish(every: 1.0, on: .main, in: .common) 22 | /// .autoconnect() 23 | /// .assign(to: &$lastUpdated) 24 | /// } 25 | /// } 26 | /// 27 | /// If you instead implemented `MyModel` with `assign(to: lastUpdated, on: self)`, storing the returned ``AnyCancellable`` instance could cause a reference cycle, because the ``Subscribers/Assign`` subscriber would hold a strong reference to `self`. Using `assign(to:)` solves this problem. 28 | /// 29 | /// While the `to` parameter uses the `inout` keyword, this method doesn't replace a reference type passed to it. Instead, this notation indicates that the operator may modify members of the assigned object, as seen in the following example: 30 | /// 31 | /// class MyModel2: ObservableObject { 32 | /// @Published var id: Int = 0 33 | /// } 34 | /// let model2 = MyModel2() 35 | /// Just(100).assign(to: &model2.$id) 36 | /// 37 | /// - Parameter published: A property marked with the `@Published` attribute, which receives and republishes all elements received from the upstream publisher. 38 | func assign(to keyPath: ReferenceWritableKeyPath, on root: Root) -> AnyCancellable { 39 | sink { [weak root] in 40 | root?[keyPath: keyPath] = $0 41 | } 42 | } 43 | 44 | /// Attaches a subscriber with closure-based behavior. 45 | /// 46 | /// Use ``Publisher/sink(receiveCompletion:receiveValue:)`` to observe values received by the publisher and process them using a closure you specify. 47 | /// 48 | /// In this example, a publisher publishes integers to a ``Publisher/sink(receiveCompletion:receiveValue:)`` operator’s `receiveValue` closure that prints them to the console. Upon completion the ``Publisher/sink(receiveCompletion:receiveValue:)`` operator’s `receiveCompletion` closure indicates the successful termination of the stream. 49 | /// 50 | /// let myRange = (0...3) 51 | /// cancellable = myRange.publisher 52 | /// .sink(receiveCompletion: { print ("completion: \($0)") }, 53 | /// receiveValue: { print ("value: \($0)") }) 54 | /// 55 | /// // Prints: 56 | /// // value: 0 57 | /// // value: 1 58 | /// // value: 2 59 | /// // value: 3 60 | /// // completion: finished 61 | /// 62 | /// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber. 63 | /// The return value should be held, otherwise the stream will be canceled. 64 | /// 65 | /// - parameter receiveComplete: The closure to execute on completion. 66 | /// - parameter receiveValue: The closure to execute on receipt of a value. 67 | /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 68 | public func sink(receiveCompletion: ((Subscribers.Completion) -> Void)? = nil) -> AnyCancellable { 69 | self.sink(receiveCompletion: receiveCompletion ?? { _ in }, receiveValue: { _ in }) 70 | } 71 | 72 | /// Attaches a subscriber with closure-based behavior. 73 | /// 74 | /// Use ``Publisher/sink(receiveCompletion:receiveValue:)`` to observe values received by the publisher and process them using a closure you specify. 75 | /// 76 | /// In this example, a publisher publishes integers to a ``Publisher/sink(receiveValue:,completion:,failure:)`` operator’s `receiveValue` closure that prints them to the console. Upon completion the ``Publisher/sink(receiveValue:,completion:,failure:)`` operator’s `completion` closure indicates the successful termination of the stream. When a failure occurs the ``Publisher/sink(receiveValue:,completion:,failure:)`` operator’s `failure` closure indicates the failure and termination of the stream. 77 | /// 78 | /// let myRange = (0...3) 79 | /// cancellable = myRange.publisher 80 | /// .sink(receiveValue: { print("value: \($0)") }, 81 | /// completion: { print("completion") }, 82 | /// failure: { print("failure: \($0)") }) 83 | /// 84 | /// // Prints: 85 | /// // value: 0 86 | /// // value: 1 87 | /// // value: 2 88 | /// // value: 3 89 | /// // completion 90 | /// 91 | /// This method creates the subscriber and immediately requests an unlimited number of values, prior to returning the subscriber. 92 | /// The return value should be held, otherwise the stream will be canceled. 93 | /// 94 | /// - parameter receiveValue: The closure to execute on receipt of a value. 95 | /// - parameter complete: The closure to execute on completion. 96 | /// - parameter failure: The closure to execute on failure. 97 | /// - Returns: A cancellable instance, which you use when you end assignment of the received value. Deallocation of the result will tear down the subscription stream. 98 | public func sink(receiveValue: ((Self.Output) -> Void)? = nil, completion: (() -> Void)? = nil, failure: ((Self.Failure) -> Void)? = nil) -> AnyCancellable { 99 | self.sink( 100 | receiveCompletion: { receivedCompletion in 101 | switch receivedCompletion { 102 | case .finished: 103 | completion?() 104 | case .failure(let error): 105 | failure?(error) 106 | } 107 | }, 108 | receiveValue: receiveValue ?? { _ in }) 109 | } 110 | 111 | public func debug(_ label: String) -> AnyPublisher { 112 | self.handleEvents( 113 | receiveSubscription: { subscription in 114 | Swift.print("\(Date()) \(label) subscribed \(subscription)") 115 | }, 116 | receiveOutput: { output in 117 | Swift.print("\(Date()) \(label) output \(output)") 118 | }, 119 | receiveCompletion: { completion in 120 | Swift.print("\(Date()) \(label) completion \(completion)") 121 | }, 122 | receiveCancel: { 123 | Swift.print("\(Date()) \(label) cancel") 124 | }, 125 | receiveRequest: { demand in 126 | Swift.print("\(Date()) \(label) request \(demand)") 127 | }) 128 | .eraseToAnyPublisher() 129 | } 130 | 131 | /// .withLatestFromFix(_:) in CombineExt is leaky for long-lived streams: https://github.com/CombineCommunity/CombineExt/issues/87 132 | /// This version fixes the stream issue (see comment on 8/6/2021 by freak4pc) https://gist.github.com/freak4pc/8d46ea6a6f5e5902c3fb5eba440a55c3 133 | func withLatestFromUnretained(_ other: Other, resultSelector: @escaping (Output, Other.Output) -> Result) -> AnyPublisher where Other.Failure == Failure { 134 | let upstream = share() 135 | return other 136 | .map { second in upstream.map { resultSelector($0, second) } } 137 | .switchToLatest() 138 | .zip(upstream) // `zip`ping and discarding `\.1` allows for 139 | // upstream completions to be projected down immediately. 140 | .map(\.0) 141 | .eraseToAnyPublisher() 142 | } 143 | 144 | /// .withLatestFromFix(_:) in CombineExt is leaky for long-lived streams: https://github.com/CombineCommunity/CombineExt/issues/87 145 | /// This version fixes the stream issue (see comment on 8/6/2021 by freak4pc) https://gist.github.com/freak4pc/8d46ea6a6f5e5902c3fb5eba440a55c3 146 | func withLatestFromUnretained(_ other: Other) -> AnyPublisher where Failure == Other.Failure { 147 | let upstream = share() 148 | return other 149 | .map { second in upstream.map { _ in second } } 150 | .switchToLatest() 151 | .zip(upstream) // `zip`ping and discarding `\.1` allows for 152 | // upstream completions to be projected down immediately. 153 | .map(\.0) 154 | .eraseToAnyPublisher() 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Extensions/Resolver+Resolved.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resolver+Resolved.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 3/21/24. 6 | // 7 | 8 | import Foundation 9 | import Swinject 10 | 11 | extension Resolver { 12 | @inlinable 13 | func resolved(_ serviceType: Service.Type) -> Service { 14 | guard let service = resolve(serviceType) else { 15 | fatalError("\(serviceType) is required for this app. Please register \(serviceType) in an Assembly.") 16 | } 17 | return service 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Extensions/String+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+Extensions.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | static var empty: String { "" } 12 | 13 | static var invisibleCharacter: String { "\u{feff}" } 14 | static func invisibleCharacters(_ count: Int) -> String { 15 | guard count > 0 else { return .empty } 16 | var out: String = .empty 17 | for _ in 1...count { 18 | out = out + .invisibleCharacter 19 | } 20 | return out 21 | } 22 | 23 | static var nonBeakingSpace: String { "\u{00a0}" } 24 | static func nonBeakingSpaces(_ count: Int) -> String { 25 | guard count > 0 else { return .empty } 26 | var out: String = .empty 27 | for _ in 1...count { 28 | out = out + .nonBeakingSpace 29 | } 30 | return out 31 | } 32 | 33 | static var space: String { " " } 34 | static func spaces(_ count: Int) -> String { 35 | guard count > 0 else { return .empty } 36 | var out: String = .empty 37 | for _ in 1...count { 38 | out = out + .space 39 | } 40 | return out 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/Extensions/UIApplication+EndEditing.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIApplication+EndEditing.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | extension UIApplication { 12 | func endEditing() { 13 | sendAction( 14 | #selector(UIResponder.resignFirstResponder), 15 | to: nil, 16 | from: nil, 17 | for: nil) 18 | } 19 | } 20 | 21 | extension View { 22 | func endEditing() { 23 | UIApplication.shared.endEditing() 24 | } 25 | } 26 | 27 | extension ViewModel { 28 | func endEditing() { 29 | UIApplication.shared.endEditing() 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/HapticFeedbackProvider.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HapticFeedbackProvider.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import UIKit 9 | import SwiftUI 10 | 11 | private let impactLightFeedbackGenerator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) 12 | private let impactMediumFeedbackGenerator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator() 13 | private let impactHeavyFeedbackGenerator: UIImpactFeedbackGenerator = UIImpactFeedbackGenerator(style: .heavy) 14 | private let selectionFeedbackGenerator: UISelectionFeedbackGenerator = UISelectionFeedbackGenerator() 15 | private let notificationFeedbackGenerator: UINotificationFeedbackGenerator = UINotificationFeedbackGenerator() 16 | 17 | enum HapticFeedbackStyle { 18 | case impactLight 19 | case impactMedium 20 | case impactHeavy 21 | case selection 22 | case notifySuccess 23 | case notifyWarning 24 | case notifyError 25 | } 26 | 27 | protocol HapticFeedbackProvider { 28 | func hapticFeedback(_ style: HapticFeedbackStyle) 29 | } 30 | 31 | extension HapticFeedbackProvider { 32 | func hapticFeedback(_ style: HapticFeedbackStyle) { 33 | Self.hapticFeedback(style) 34 | } 35 | 36 | static func hapticFeedback(_ style: HapticFeedbackStyle) { 37 | DispatchQueue.main.async { 38 | switch style { 39 | case .impactLight: 40 | impactLightFeedbackGenerator.impactOccurred() 41 | case .impactMedium: 42 | impactMediumFeedbackGenerator.impactOccurred() 43 | case .impactHeavy: 44 | impactHeavyFeedbackGenerator.impactOccurred() 45 | case .selection: 46 | selectionFeedbackGenerator.selectionChanged() 47 | case .notifySuccess: 48 | notificationFeedbackGenerator.notificationOccurred(.success) 49 | case .notifyWarning: 50 | notificationFeedbackGenerator.notificationOccurred(.warning) 51 | case .notifyError: 52 | notificationFeedbackGenerator.notificationOccurred(.error) 53 | } 54 | } 55 | } 56 | } 57 | 58 | struct HapticFeedbackViewProxy: HapticFeedbackProvider { 59 | func generate(_ style: HapticFeedbackStyle) { 60 | Self.hapticFeedback(style) 61 | } 62 | } 63 | 64 | extension View { 65 | var haptics: HapticFeedbackViewProxy { 66 | HapticFeedbackViewProxy() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/SwiftUI/Button+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Button+Extensions.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | extension Button { 12 | init(action: PassthroughSubject, @ViewBuilder label: () -> Label) { 13 | self.init(action: { action.send() }, label: label) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/SwiftUI/Color+SystemColors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Color+SystemColors.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | 9 | import UIKit 10 | import SwiftUI 11 | 12 | extension Color { 13 | static var label: Color { 14 | return Color(UIColor.label) 15 | } 16 | 17 | static var secondaryLabel: Color { 18 | return Color(UIColor.secondaryLabel) 19 | } 20 | 21 | static var tertiaryLabel: Color { 22 | return Color(UIColor.tertiaryLabel) 23 | } 24 | 25 | static var quaternaryLabel: Color { 26 | return Color(UIColor.quaternaryLabel) 27 | } 28 | 29 | static var systemFill: Color { 30 | return Color(UIColor.systemFill) 31 | } 32 | 33 | static var secondarySystemFill: Color { 34 | return Color(UIColor.secondarySystemFill) 35 | } 36 | 37 | static var tertiarySystemFill: Color { 38 | return Color(UIColor.tertiarySystemFill) 39 | } 40 | 41 | static var quaternarySystemFill: Color { 42 | return Color(UIColor.quaternarySystemFill) 43 | } 44 | 45 | static var systemBackground: Color { 46 | return Color(UIColor.systemBackground) 47 | } 48 | 49 | static var secondarySystemBackground: Color { 50 | return Color(UIColor.secondarySystemBackground) 51 | } 52 | 53 | static var tertiarySystemBackground: Color { 54 | return Color(UIColor.tertiarySystemBackground) 55 | } 56 | 57 | static var systemGroupedBackground: Color { 58 | return Color(UIColor.systemGroupedBackground) 59 | } 60 | 61 | static var secondarySystemGroupedBackground: Color { 62 | return Color(UIColor.secondarySystemGroupedBackground) 63 | } 64 | 65 | static var tertiarySystemGroupedBackground: Color { 66 | return Color(UIColor.tertiarySystemGroupedBackground) 67 | } 68 | 69 | static var systemRed: Color { 70 | return Color(UIColor.systemRed) 71 | } 72 | 73 | static var systemBlue: Color { 74 | return Color(UIColor.systemBlue) 75 | } 76 | 77 | static var systemPink: Color { 78 | return Color(UIColor.systemPink) 79 | } 80 | 81 | static var systemTeal: Color { 82 | return Color(UIColor.systemTeal) 83 | } 84 | 85 | static var systemGreen: Color { 86 | return Color(UIColor.systemGreen) 87 | } 88 | 89 | static var systemIndigo: Color { 90 | return Color(UIColor.systemIndigo) 91 | } 92 | 93 | static var systemOrange: Color { 94 | return Color(UIColor.systemOrange) 95 | } 96 | 97 | static var systemPurple: Color { 98 | return Color(UIColor.systemPurple) 99 | } 100 | 101 | static var systemYellow: Color { 102 | return Color(UIColor.systemYellow) 103 | } 104 | 105 | static var systemGray: Color { 106 | return Color(UIColor.systemGray) 107 | } 108 | 109 | static var systemGray2: Color { 110 | return Color(UIColor.systemGray2) 111 | } 112 | 113 | static var systemGray3: Color { 114 | return Color(UIColor.systemGray3) 115 | } 116 | 117 | static var systemGray4: Color { 118 | return Color(UIColor.systemGray4) 119 | } 120 | 121 | static var systemGray5: Color { 122 | return Color(UIColor.systemGray5) 123 | } 124 | 125 | static var systemGray6: Color { 126 | return Color(UIColor.systemGray6) 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/SwiftUI/EdgeInsets+Extensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EdgeInsets+Extensions.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 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 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/SwiftUI/ObjectNavigationStack.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ObjectNavigationStack.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 12/5/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ObjectNavigationStack: View where Content : View { 11 | @State var path: ObjectNavigationPath 12 | let content: () -> Content 13 | 14 | var body: some View { 15 | NavigationStack(path: self.$path.path, root: self.content) 16 | } 17 | } 18 | 19 | @Observable 20 | class ObjectNavigationPath { 21 | typealias NavigationObject = AnyObject & Hashable & Equatable 22 | fileprivate var path: NavigationPath = NavigationPath() 23 | private var objects: [any NavigationObject] = [] 24 | 25 | private let semaphore: DispatchSemaphore = DispatchSemaphore(value: 1) 26 | 27 | var last: (any NavigationObject)? { 28 | self.objects.last 29 | } 30 | 31 | func append(_ object: some NavigationObject) { 32 | self.semaphore.wait() 33 | self.objects.append(object) 34 | self.path.append(object) 35 | self.semaphore.signal() 36 | } 37 | 38 | func removeLast() { 39 | self.semaphore.wait() 40 | self.objects.removeLast() 41 | self.path.removeLast() 42 | self.semaphore.signal() 43 | } 44 | 45 | @discardableResult 46 | func removeLast(through graphObject: Element) -> Element? { 47 | self.semaphore.wait() 48 | var removeCount: Int = 0 49 | defer { 50 | self.path.removeLast(removeCount) 51 | self.semaphore.signal() 52 | } 53 | 54 | while let object = self.objects.popLast() { 55 | removeCount = removeCount + 1 56 | if graphObject === object { 57 | return graphObject 58 | } 59 | } 60 | return nil 61 | } 62 | 63 | @discardableResult 64 | func removeLast(through clause: (any NavigationObject) -> Bool) -> (any NavigationObject)? { 65 | self.semaphore.wait() 66 | var removeCount: Int = 0 67 | defer { 68 | self.path.removeLast(removeCount) 69 | self.semaphore.signal() 70 | } 71 | 72 | while let object = self.objects.popLast() { 73 | removeCount = removeCount + 1 74 | if clause(object) { 75 | return object 76 | } 77 | } 78 | return nil 79 | } 80 | } 81 | 82 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Core/SwiftUI/View+OnReceive.swift: -------------------------------------------------------------------------------- 1 | // 2 | // View+OnReceive.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | extension View { 12 | /// Adds an action to perform when this view detects data emitted by the 13 | /// given publisher. 14 | /// 15 | /// - Parameters: 16 | /// - publisher: The publisher to subscribe to. 17 | /// - animation: The animation for the received publisher. 18 | /// - action: The action to perform when an event is emitted by 19 | /// `publisher`. The event emitted by publisher is passed as a 20 | /// parameter to `action`. 21 | /// 22 | /// - Returns: A view that triggers `action` when `publisher` emits an 23 | /// event. 24 | @inlinable public func onReceive

(_ publisher: P, withAnimation animation: Animation?, perform action: @escaping (P.Output) -> Void) -> some View where P : Publisher, P.Failure == Never { 25 | self.onReceive(publisher) { value in 26 | withAnimation(animation) { 27 | action(value) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/MVVM_Demo_SwiftUIApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVM_Demo_SwiftUIApp.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | import BusyIndicator 10 | 11 | private let appAssembler: AppAssembler = AppAssembler() 12 | 13 | @main 14 | struct MVVM_Demo_SwiftUIApp: App { 15 | var body: some Scene { 16 | WindowGroup { 17 | AppRootCoordinatorView( 18 | coordinator: appAssembler.resolver.resolved(AppRootCoordinator.self) 19 | ) 20 | .alertManager(appAssembler.resolver.resolved(AlertManager.self)) 21 | .busyIndicator(appAssembler.resolver.resolved(BusyIndicatorServiceProtocol.self).busyIndicator) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Services/AlertService/AlertService.AlertPackage+View.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertService.AlertPackage+View.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 2/28/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | extension AlertService.AlertPackage { 11 | func alert() -> SwiftUI.Alert { 12 | if let secondaryButton = self.secondaryButton { 13 | return SwiftUI.Alert( 14 | title: Text(self.title), 15 | message: self.message == nil ? nil : Text(self.message!), 16 | primaryButton: button(self.primaryButton), 17 | secondaryButton: button(secondaryButton)) 18 | } else { 19 | return SwiftUI.Alert( 20 | title: Text(self.title), 21 | message: self.message == nil ? nil : Text(self.message!), 22 | dismissButton: button(self.primaryButton)) 23 | } 24 | } 25 | 26 | private func button(_ button: AlertService.AlertPackage.Button) -> SwiftUI.Alert.Button { 27 | switch button.role { 28 | case .default: 29 | return .default(Text(button.title), action: button.action) 30 | case .cancel: 31 | return .cancel(Text(button.title), action: button.action) 32 | case .destructive: 33 | return .destructive(Text(button.title), action: button.action) 34 | } 35 | } 36 | } 37 | 38 | // MARK: ButtonRole 39 | 40 | extension AlertService.AlertPackage.Button.Role { 41 | var buttonRole: ButtonRole? { 42 | switch self { 43 | case .default: return nil 44 | case .destructive: return .destructive 45 | case .cancel: return .cancel 46 | } 47 | } 48 | } 49 | 50 | // MARK: View 51 | 52 | extension View { 53 | /// Present an iOS system-style alert from SwiftUI Coordinator pattern. 54 | func navigationAlert(item: Binding) -> some View { 55 | let isActive = Binding( 56 | get: { item.wrappedValue != nil }, 57 | set: { value in 58 | if !value { 59 | item.wrappedValue = nil 60 | } 61 | } 62 | ) 63 | return self.alert( 64 | item.wrappedValue?.title ?? .empty, 65 | isPresented: isActive, 66 | actions: { 67 | if let secondary = item.wrappedValue?.secondaryButton { 68 | Button(secondary.title, role: secondary.role.buttonRole, action: { secondary.action?() }) 69 | } 70 | if let primary = item.wrappedValue?.primaryButton { 71 | Button(primary.title, role: primary.role.buttonRole, action: { primary.action?() }) 72 | } 73 | }, 74 | message: { 75 | if let message = item.wrappedValue?.message { 76 | Text(message) 77 | } 78 | }) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Services/AlertService/AlertService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertService.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | 11 | // MARK: AlertManager 12 | @Observable 13 | public class AlertManager: Equatable { 14 | var alert: AlertService.AlertPackage? { 15 | didSet { self.alert$.send(self.alert) } 16 | } 17 | let alert$: CurrentValueSubject = CurrentValueSubject(nil) 18 | 19 | static public func ==(lhs: AlertManager, rhs: AlertManager) -> Bool { 20 | lhs === rhs 21 | } 22 | } 23 | 24 | // MARK: AlertServiceProtocol 25 | protocol AlertServiceProtocol: AnyObject { 26 | func present(title: String, message: String?, dismissButton: AlertService.AlertPackage.Button?) 27 | func present(title: String, message: String?, primaryButton: AlertService.AlertPackage.Button, secondaryButton: AlertService.AlertPackage.Button) 28 | } 29 | 30 | extension AlertServiceProtocol { 31 | func present(title: String, message: String? = nil, dismissButton: AlertService.AlertPackage.Button? = nil) { 32 | present(title: title, message: message, dismissButton: dismissButton) 33 | } 34 | 35 | func present(title: String, message: String? = nil, primaryButton: AlertService.AlertPackage.Button, secondaryButton: AlertService.AlertPackage.Button) { 36 | present(title: title, message: message, primaryButton: primaryButton, secondaryButton: secondaryButton) 37 | } 38 | } 39 | 40 | class AlertService: AlertServiceProtocol { 41 | private let manager: AlertManager 42 | 43 | init(alertManager: AlertManager) { 44 | self.manager = alertManager 45 | } 46 | 47 | func present(title: String, message: String?, dismissButton: AlertService.AlertPackage.Button?) { 48 | let alert = AlertPackage(title: title, message: message, dismissButton: dismissButton) 49 | DispatchQueue.main.async { 50 | self.manager.alert = alert 51 | } 52 | } 53 | 54 | func present(title: String, message: String?, primaryButton: AlertService.AlertPackage.Button, secondaryButton: AlertService.AlertPackage.Button) { 55 | let alert = AlertPackage(title: title, message: message, primaryButton: primaryButton, secondaryButton: secondaryButton) 56 | DispatchQueue.main.async { 57 | self.manager.alert = alert 58 | } 59 | } 60 | } 61 | 62 | extension AlertService { 63 | struct AlertPackage: Identifiable, Equatable { 64 | let id: UUID = UUID() 65 | let title: String 66 | let message: String? 67 | let primaryButton: AlertService.AlertPackage.Button 68 | let secondaryButton: AlertService.AlertPackage.Button? 69 | 70 | init(title: String, message: String? = nil, dismissButton: AlertService.AlertPackage.Button? = nil) { 71 | self.title = title 72 | self.message = message 73 | self.primaryButton = dismissButton ?? .cancel() 74 | self.secondaryButton = nil 75 | } 76 | 77 | init(title: String, message: String? = nil, primaryButton: AlertService.AlertPackage.Button, secondaryButton: AlertService.AlertPackage.Button) { 78 | self.title = title 79 | self.message = message 80 | self.primaryButton = primaryButton 81 | self.secondaryButton = secondaryButton 82 | } 83 | 84 | static func ==(lhs: AlertPackage, rhs: AlertPackage) -> Bool { 85 | lhs.id == rhs.id 86 | } 87 | } 88 | } 89 | 90 | extension AlertService.AlertPackage { 91 | struct Button { 92 | enum Role { 93 | case `default` 94 | case cancel 95 | case destructive 96 | } 97 | 98 | let role: Role 99 | let title: String 100 | let action: (() -> Void)? 101 | 102 | static func `default`(_ title: String, action: (() -> Void)?) -> AlertService.AlertPackage.Button { 103 | AlertService.AlertPackage.Button(role: .default, title: title, action: action) 104 | } 105 | 106 | static func cancel(_ title: String = "Cancel", action: (() -> Void)? = nil) -> AlertService.AlertPackage.Button { 107 | AlertService.AlertPackage.Button(role: .cancel, title: title, action: action) 108 | } 109 | 110 | static func destructive(_ title: String, action: (() -> Void)?) -> AlertService.AlertPackage.Button { 111 | AlertService.AlertPackage.Button(role: .destructive, title: title, action: action) 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Services/AuthenticationService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationService.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import CombineExt 11 | import SwiftUI 12 | import BusyIndicator 13 | 14 | protocol AuthenticationServiceProtocol: AnyObject { 15 | var user: AnyPublisher { get } 16 | var isAuthenticated: AnyPublisher { get } 17 | 18 | func signIn(username: String, password: String) -> AnyPublisher 19 | func signOut() -> AnyPublisher 20 | func signOutAsync() 21 | } 22 | 23 | class AuthenticationService: AuthenticationServiceProtocol { 24 | private let busyIndicatorService: BusyIndicatorServiceProtocol 25 | 26 | private let _user: CurrentValueSubject = CurrentValueSubject(nil) 27 | var user: AnyPublisher { self._user.eraseToAnyPublisher() } 28 | 29 | lazy private(set) var isAuthenticated: AnyPublisher = self._user 30 | .map { return $0 != nil } 31 | .share(replay: 1) 32 | .eraseToAnyPublisher() 33 | 34 | private var cancelBag = CancelBag() 35 | 36 | init(busyIndicatorService: BusyIndicatorServiceProtocol) { 37 | self.busyIndicatorService = busyIndicatorService 38 | } 39 | 40 | func signIn(username: String, password: String) -> AnyPublisher { 41 | let user = User(username: username, password: password) 42 | self._user.send(user) 43 | return self._user 44 | .prefix(1) 45 | .setFailureType(to: Error.self) 46 | .flatMapLatest { (newUser: User?) -> AnyPublisher in 47 | if newUser == user { 48 | return Just(user) 49 | .setFailureType(to: Error.self) 50 | .eraseToAnyPublisher() 51 | } else { 52 | return Fail(error: Errors.userIsOutOfSync) 53 | .eraseToAnyPublisher() 54 | } 55 | } 56 | .eraseToAnyPublisher() 57 | } 58 | 59 | func signOut() -> AnyPublisher { 60 | let busySubject = self.busyIndicatorService.enqueue() 61 | self._user.send(nil) 62 | return self._user 63 | .prefix(1) 64 | .setFailureType(to: Error.self) 65 | // This delay is to the the BusyIndicator at work. 66 | .delay(for: 3.0, scheduler: DispatchQueue.global(qos: .userInitiated)) 67 | .flatMapLatest { (newUser: User?) -> AnyPublisher in 68 | defer { busySubject.dequeue() } 69 | if newUser == nil { 70 | return Just() 71 | .setFailureType(to: Error.self) 72 | .eraseToAnyPublisher() 73 | } else { 74 | return Fail(error: Errors.userIsOutOfSync) 75 | .eraseToAnyPublisher() 76 | } 77 | } 78 | .eraseToAnyPublisher() 79 | } 80 | 81 | func signOutAsync() { 82 | self.signOut() 83 | .debug("## signout") 84 | .sink() 85 | .store(in: &self.cancelBag) 86 | } 87 | } 88 | 89 | extension AuthenticationService { 90 | enum Errors: Error { 91 | case userIsOutOfSync 92 | } 93 | } 94 | 95 | struct User: Equatable { 96 | let username: String 97 | let password: String 98 | 99 | static func ==(lhs: User, rhs: User) -> Bool { 100 | return lhs.username == rhs.username 101 | && lhs.password == rhs.password 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/Services/ColorService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorService.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | import Combine 10 | 11 | enum ColorModel { 12 | case blue 13 | case green 14 | case orange 15 | case pink 16 | case purple 17 | case red 18 | case yellow 19 | case white 20 | } 21 | 22 | extension ColorModel { 23 | func asColor() -> Color { 24 | switch self { 25 | case .blue: return .blue 26 | case .green: return .green 27 | case .orange: return .orange 28 | case .pink: return .pink 29 | case .purple: return .purple 30 | case .red: return .red 31 | case .white: return .white 32 | case .yellow: return .yellow 33 | } 34 | } 35 | 36 | func asContrastColor() -> Color { 37 | switch self { 38 | case .blue: return .white 39 | case .green: return .black 40 | case .orange: return .white 41 | case .pink: return .white 42 | case .purple: return .white 43 | case .red: return .white 44 | case .white: return .black 45 | case .yellow: return .black 46 | } 47 | } 48 | } 49 | 50 | protocol ColorServiceProtocol: AnyObject { 51 | func getNextColor() -> ColorModel 52 | func generateColors(runLoop: RunLoop) -> AnyPublisher 53 | } 54 | 55 | extension ColorServiceProtocol { 56 | func generateColors(runLoop: RunLoop = .main) -> AnyPublisher { 57 | generateColors(runLoop: runLoop) 58 | } 59 | } 60 | 61 | class ColorService: ColorServiceProtocol { 62 | private var index: Int = 0 63 | 64 | func getNextColor() -> ColorModel { 65 | let selection = self.index % 7 66 | self.index = self.index + 1 67 | switch selection { 68 | case 0: return .blue 69 | case 1: return .green 70 | case 2: return .orange 71 | case 3: return .pink 72 | case 4: return .purple 73 | case 5: return .red 74 | case 6: return .yellow 75 | default: return .white 76 | } 77 | } 78 | 79 | func generateColors(runLoop: RunLoop = .main) -> AnyPublisher { 80 | return Timer.publish(every: 1.0, on: runLoop, in: .default) 81 | .autoconnect() 82 | .map { timer in 83 | let selection = Int(timer.timeIntervalSince1970 * 1.5) % 7 84 | switch selection { 85 | case 0: return .blue 86 | case 1: return .green 87 | case 2: return .orange 88 | case 3: return .pink 89 | case 4: return .purple 90 | case 5: return .red 91 | case 6: return .yellow 92 | default: return .white 93 | } 94 | } 95 | .eraseToAnyPublisher() 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/AppRootCoordinator/AppRootCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppRootCoordinator.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Swinject 11 | 12 | @Observable 13 | class AppRootCoordinator: ViewModel { 14 | private let resolver: Resolver 15 | 16 | private(set) var landingViewModel: LandingViewModel! 17 | 18 | let path = ObjectNavigationPath() 19 | 20 | var signInViewModel: SignInViewModel? 21 | var infiniteCardsViewModel: InfiniteCardsViewModel? 22 | var colorWizardCoordinator: ColorWizardCoordinator? 23 | 24 | init(resolver: Resolver) { 25 | self.resolver = resolver 26 | 27 | self.landingViewModel = self.resolver.resolved(LandingViewModel.self) 28 | .setup(delegate: self) 29 | } 30 | } 31 | 32 | // MARK: LandingViewModelDelegate 33 | extension AppRootCoordinator: LandingViewModelDelegate { 34 | func landingViewModelDidTapPulse(_ source: LandingViewModel) { 35 | self.path.append(self.resolver.resolved(PulseViewModel.self) 36 | .setup(delegate: self)) 37 | } 38 | 39 | func landingViewModelDidTapSignIn(_ source: LandingViewModel) { 40 | self.signInViewModel = self.resolver.resolved(SignInViewModel.self) 41 | .setup(delegate: self) 42 | } 43 | 44 | func landingViewModelDidTapInfiniteCards(_ source: LandingViewModel) { 45 | self.infiniteCardsViewModel = self.resolver.resolved(InfiniteCardsViewModel.self) 46 | .setup(delegate: self) 47 | } 48 | 49 | func landingViewModelDidTapColorWizard(_ source: LandingViewModel) { 50 | self.colorWizardCoordinator = self.resolver.resolved(ColorWizardCoordinator.self) 51 | .setup(configuration: ColorWizardConfiguration.mock(), delegate: self) 52 | } 53 | } 54 | 55 | // MARK: SignInViewModelDelegate 56 | extension AppRootCoordinator: SignInViewModelDelegate { 57 | func signInViewModelDidCancel(_ source: SignInViewModel) { 58 | self.signInViewModel = nil 59 | } 60 | 61 | func signInViewModelDidSignIn(_ source: SignInViewModel) { 62 | self.signInViewModel = nil 63 | } 64 | } 65 | 66 | // MARK: PulseViewModelDelegate 67 | extension AppRootCoordinator: PulseViewModelDelegate { 68 | // Nothing yet 69 | } 70 | 71 | // MARK: InfiniteCardsViewModelDelegate 72 | extension AppRootCoordinator: InfiniteCardsViewModel.Delegate { 73 | func infiniteCardsViewModelDidClose(_ sender: InfiniteCardsViewModel) { 74 | self.infiniteCardsViewModel = nil 75 | } 76 | } 77 | 78 | // MARK: ColorWizardCoordinatorDelegate 79 | extension AppRootCoordinator: ColorWizardCoordinatorDelegate { 80 | func colorWizardCoordinatorDidComplete(_ source: ColorWizardCoordinator) { 81 | self.colorWizardCoordinator = nil 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/AppRootCoordinator/AppRootCoordinatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppRootCoordinatorView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct AppRootCoordinatorView: View { 11 | @Environment(\.alertManager) var alertManager: AlertManager 12 | 13 | @Bindable var coordinator: AppRootCoordinator 14 | 15 | @State private var colorWizardCoordinator: ColorWizardCoordinator? 16 | @State private var infiniteCardsViewModel: InfiniteCardsViewModel? 17 | @State private var signInViewModel: SignInViewModel? 18 | @State private var alert: AlertService.AlertPackage? 19 | 20 | var body: some View { 21 | ObjectNavigationStack(path: self.coordinator.path) { 22 | LandingView(viewModel: self.coordinator.landingViewModel) 23 | .navigationDestination(for: PulseViewModel.self) { 24 | PulseView(viewModel: $0) 25 | } 26 | .fullScreenCover(item: self.$infiniteCardsViewModel) { viewModel in 27 | NavigationStack { 28 | InfiniteCardsView(viewModel: viewModel) 29 | } 30 | } 31 | .fullScreenCover(item: self.$colorWizardCoordinator) { coordinator in 32 | ColorWizardCoordinatorView(coordinator: coordinator) 33 | } 34 | } 35 | .overlay { 36 | if let viewModel = self.signInViewModel { 37 | SignInView(viewModel: viewModel) 38 | } 39 | } 40 | .navigationAlert(item: self.$alert) 41 | .onChange(of: self.coordinator.colorWizardCoordinator, initial: true) { _, value in 42 | self.colorWizardCoordinator = value 43 | } 44 | .onChange(of: self.coordinator.infiniteCardsViewModel, initial: true) { _, value in 45 | self.infiniteCardsViewModel = value 46 | } 47 | .onChange(of: self.alertManager.alert, initial: true) { _, value in self.alert = value } 48 | .onChange(of: self.coordinator.signInViewModel, initial: true) { _, value in 49 | self.signInViewModel = value 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/ColorWizardCoordinator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardCoordinator.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import Swinject 11 | 12 | protocol ColorWizardCoordinatorDelegate: AnyObject { 13 | func colorWizardCoordinatorDidComplete(_ source: ColorWizardCoordinator) 14 | } 15 | 16 | @Observable 17 | class ColorWizardCoordinator: ViewModel { 18 | private let resolver: Resolver 19 | 20 | private weak var delegate: ColorWizardCoordinatorDelegate? 21 | private var configurationViewModel: ColorWizardConfigurationViewModel! 22 | 23 | let path = ObjectNavigationPath() 24 | 25 | private(set) var rootContentViewModel: ColorWizardContentViewModel! 26 | 27 | init(resolver: Resolver) { 28 | self.resolver = resolver 29 | } 30 | 31 | func setup(configuration: ColorWizardConfiguration, delegate: ColorWizardCoordinatorDelegate) -> Self { 32 | self.delegate = delegate 33 | self.configurationViewModel = ColorWizardConfigurationViewModel(configuration: configuration) 34 | 35 | if let firstPageViewModel = self.configurationViewModel.pages.first { 36 | self.rootContentViewModel = self.resolver.resolved(ColorWizardContentViewModel.self) 37 | .setup(pageViewModel: firstPageViewModel, delegate: self) 38 | } else { 39 | fatalError() 40 | } 41 | 42 | return self 43 | } 44 | } 45 | 46 | // MARK: ColorWizardContentViewModelDelegate 47 | extension ColorWizardCoordinator: ColorWizardContentViewModelDelegate { 48 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, canMoveBackFromIndex index: Int) -> Bool { 49 | return index != 0 50 | } 51 | 52 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, canMoveForwardFromIndex index: Int) -> Bool { 53 | return self.configurationViewModel.pages.count > index + 1 54 | } 55 | 56 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, canCompleteFromIndex index: Int) -> Bool { 57 | return self.configurationViewModel.pages.count == index + 1 58 | } 59 | 60 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, didMoveBackFromIndex index: Int) { 61 | self.path.removeLast(through: source) 62 | } 63 | 64 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, didMoveForwardFromIndex index: Int) { 65 | let newIndex = index + 1 66 | guard newIndex < self.configurationViewModel.pages.count else { 67 | fatalError() 68 | } 69 | let nextPageViewModel = self.configurationViewModel.pages[newIndex] 70 | 71 | self.path.append(self.resolver.resolved(ColorWizardContentViewModel.self) 72 | .setup(pageViewModel: nextPageViewModel, delegate: self)) 73 | } 74 | 75 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, didCompleteFromIndex index: Int) { 76 | self.delegate?.colorWizardCoordinatorDidComplete(self) 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/ColorWizardCoordinatorView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardCoordinatorView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorWizardCoordinatorView: View { 11 | @Bindable var coordinator: ColorWizardCoordinator 12 | 13 | var body: some View { 14 | ObjectNavigationStack(path: self.coordinator.path) { 15 | ColorWizardContentView(viewModel: self.coordinator.rootContentViewModel) 16 | .navigationDestination(for: ColorWizardContentViewModel.self) { 17 | ColorWizardContentView(viewModel: $0) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/Configuration/ColorWizardConfiguration+Mock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardConfiguration+Mock.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 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: .systemIndigo), 16 | .page("Fourth Color", color: .pink), 17 | .page("Fifth Color", color: .purple), 18 | .page("Summary", colors: [ 19 | .green, 20 | .orange, 21 | .systemIndigo, 22 | .pink, 23 | .purple, 24 | ]), 25 | ]) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/Configuration/ColorWizardConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardConfiguration.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorWizardConfiguration { 11 | let pages: [Page] 12 | } 13 | 14 | extension ColorWizardConfiguration { 15 | struct Page { 16 | let title: String 17 | let color: Color? 18 | let colors: [Color] 19 | 20 | static func page(_ title: String, color: Color? = nil, colors: [Color] = []) -> Page { 21 | Page(title: title, color: color, colors: colors) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/Configuration/ColorWizardConfigurationViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardConfigurationViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | @Observable 13 | class ColorWizardConfigurationViewModel: ViewModel { 14 | let pages: [PageViewModel] 15 | 16 | init(configuration: ColorWizardConfiguration) { 17 | var pageViewModels: [PageViewModel] = [] 18 | for (index, page) in configuration.pages.enumerated() { 19 | pageViewModels.append(PageViewModel(page: page, index: index)) 20 | } 21 | self.pages = pageViewModels 22 | } 23 | } 24 | 25 | extension ColorWizardConfigurationViewModel { 26 | @Observable 27 | class PageViewModel: ViewModel { 28 | let index: Int 29 | let title: String 30 | let color: Color? 31 | let colors: [ColorViewModel] 32 | 33 | init(page: ColorWizardConfiguration.Page, index: Int) { 34 | self.index = index 35 | self.title = page.title 36 | self.color = page.color 37 | self.colors = page.colors.map { ColorViewModel(color: $0) } 38 | } 39 | } 40 | 41 | @Observable 42 | class ColorViewModel: ViewModel { 43 | let id: String = UUID().uuidString 44 | let color: Color 45 | 46 | init(color: Color) { 47 | self.color = color 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/Content/ColorWizardContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardContentView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct ColorWizardContentView: View { 11 | @Bindable var viewModel: ColorWizardContentViewModel 12 | 13 | var body: some View { 14 | ZStack { 15 | if let color = self.viewModel.color { 16 | Color.clear 17 | .background(color) 18 | } else { 19 | ScrollView { 20 | VStack { 21 | ForEach(self.viewModel.colors, id: \.id) { color in 22 | RoundedRectangle(cornerRadius: 36.0, style: .continuous) 23 | .fill(color.color) 24 | .frame(maxWidth: .infinity, minHeight: 54.0, idealHeight: 54.0, maxHeight: 54.0) 25 | .padding([.leading, .trailing], 16.0) 26 | } 27 | } 28 | } 29 | } 30 | } 31 | .frame(maxWidth: .infinity, maxHeight: .infinity) 32 | .navigationTitle(self.viewModel.title) 33 | .navigationBarBackButtonHidden(true) 34 | .toolbar { 35 | ToolbarItem(placement: .navigationBarLeading) { 36 | if self.viewModel.canMoveBack { 37 | Button(action: { self.viewModel.moveBack() }) { 38 | Text("Back") 39 | } 40 | } 41 | } 42 | ToolbarItem(placement: .navigationBarTrailing) { 43 | if self.viewModel.canMoveForward { 44 | Button(action: { self.viewModel.moveForward() }) { 45 | Text("Forward") 46 | } 47 | } else if self.viewModel.canComplete { 48 | Button(action: { self.viewModel.complete() }) { 49 | Text("Done") 50 | } 51 | } 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/ColorWizard/Content/ColorWizardContentViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorWizardContentViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/17/21. 6 | // 7 | 8 | import Foundation 9 | import SwiftUI 10 | 11 | protocol ColorWizardContentViewModelDelegate: AnyObject { 12 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, canMoveBackFromIndex index: Int) -> Bool 13 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, canMoveForwardFromIndex index: Int) -> Bool 14 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, canCompleteFromIndex index: Int) -> Bool 15 | 16 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, didMoveBackFromIndex index: Int) 17 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, didMoveForwardFromIndex index: Int) 18 | func colorWizardContentViewModel(_ source: ColorWizardContentViewModel, didCompleteFromIndex index: Int) 19 | } 20 | 21 | @Observable 22 | class ColorWizardContentViewModel: ViewModel { 23 | private var pageViewModel: ColorWizardConfigurationViewModel.PageViewModel! 24 | private weak var delegate: ColorWizardContentViewModelDelegate? 25 | 26 | var index: Int { self.pageViewModel.index } 27 | var title: String { self.pageViewModel.title } 28 | var color: Color? { self.pageViewModel.color } 29 | var colors: [ColorWizardConfigurationViewModel.ColorViewModel] { self.pageViewModel.colors } 30 | 31 | var canMoveBack: Bool { 32 | self.delegate?.colorWizardContentViewModel(self, canMoveBackFromIndex: self.index) ?? false 33 | } 34 | 35 | var canMoveForward: Bool { 36 | self.delegate?.colorWizardContentViewModel(self, canMoveForwardFromIndex: self.index) ?? false 37 | } 38 | 39 | var canComplete: Bool { 40 | self.delegate?.colorWizardContentViewModel(self, canCompleteFromIndex: self.index) ?? false 41 | } 42 | 43 | @discardableResult 44 | func setup(pageViewModel: ColorWizardConfigurationViewModel.PageViewModel, delegate: ColorWizardContentViewModelDelegate?) -> Self { 45 | self.pageViewModel = pageViewModel 46 | self.delegate = delegate 47 | return self 48 | } 49 | 50 | func moveBack() { 51 | self.delegate?.colorWizardContentViewModel(self, didMoveBackFromIndex: self.index) 52 | } 53 | 54 | func moveForward() { 55 | self.delegate?.colorWizardContentViewModel(self, didMoveForwardFromIndex: self.index) 56 | } 57 | 58 | func complete() { 59 | self.delegate?.colorWizardContentViewModel(self, didCompleteFromIndex: self.index) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/InfiniteCards/InfiniteCardsView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCardsView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/29/24. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct InfiniteCardsView: View { 11 | @Bindable var viewModel: InfiniteCardsViewModel 12 | 13 | var body: some View { 14 | ScrollView(.horizontal) { 15 | LazyHStack(spacing: 0.0) { 16 | ForEach(self.viewModel.cards) { card in 17 | CardView(viewModel: card) 18 | .task { 19 | guard card.shouldFetchNextBatch else { 20 | return 21 | } 22 | self.viewModel.fetchNextBatch(currentCardViewModel: card) 23 | } 24 | .id(card.id) 25 | } 26 | } 27 | .scrollTargetLayout() 28 | } 29 | .scrollTargetBehavior(.paging) 30 | .navigationTitle("Infinite Cards") 31 | .toolbar { 32 | ToolbarItem(placement: .topBarTrailing) { 33 | Button { 34 | self.viewModel.close() 35 | } label: { 36 | Image(systemName: "xmark") 37 | } 38 | } 39 | } 40 | } 41 | } 42 | 43 | extension InfiniteCardsView { 44 | struct CardView: View { 45 | @ScaledMetric var cardPadding: CGFloat = 8.0 46 | @ScaledMetric private var cornerRadius: CGFloat = 16.0 47 | 48 | @Bindable var viewModel: InfiniteCardsViewModel.CardViewModel 49 | 50 | var body: some View { 51 | RoundedRectangle(cornerRadius: self.cornerRadius, style: .continuous) 52 | .fill(self.viewModel.colorModel.asColor()) 53 | .overlay(alignment: .bottomLeading) { 54 | Text(self.viewModel.id.uuidString) 55 | .minimumScaleFactor(0.5) 56 | .font(.headline) 57 | .fontDesign(.monospaced) 58 | .lineLimit(1) 59 | .bold() 60 | .foregroundStyle(self.viewModel.colorModel.asContrastColor()) 61 | .padding(self.cardPadding) 62 | } 63 | .padding(self.cardPadding) 64 | .containerRelativeFrame(.horizontal) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/InfiniteCards/InfiniteCardsViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // InfiniteCardsViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 4/29/24. 6 | // 7 | 8 | import Foundation 9 | 10 | @Observable 11 | class InfiniteCardsViewModel: ViewModel { 12 | private let colorService: ColorServiceProtocol 13 | 14 | private weak var delegate: InfiniteCardsViewModel.Delegate? 15 | 16 | private(set) var cards: [CardViewModel] = [] 17 | 18 | init(colorService: ColorServiceProtocol) { 19 | self.colorService = colorService 20 | fetchNextBatch() 21 | } 22 | 23 | func setup(delegate: InfiniteCardsViewModel.Delegate) -> Self { 24 | self.delegate = delegate 25 | return self 26 | } 27 | 28 | func close() { 29 | self.delegate?.infiniteCardsViewModelDidClose(self) 30 | } 31 | 32 | func fetchNextBatch(currentCardViewModel: CardViewModel? = nil) { 33 | currentCardViewModel?.shouldFetchNextBatch = false 34 | 35 | var cards: [CardViewModel] = Array(self.cards) 36 | for i in 0..<5 { 37 | cards.append(CardViewModel( 38 | colorModel: self.colorService.getNextColor(), 39 | shouldFetchNextBatch: i == 3)) 40 | } 41 | self.cards = cards 42 | } 43 | } 44 | 45 | extension InfiniteCardsViewModel { 46 | protocol Delegate: AnyObject { 47 | func infiniteCardsViewModelDidClose(_ sender: InfiniteCardsViewModel) 48 | } 49 | } 50 | 51 | extension InfiniteCardsViewModel { 52 | @Observable 53 | class CardViewModel: Identifiable { 54 | let id = UUID() 55 | let colorModel: ColorModel 56 | var shouldFetchNextBatch: Bool 57 | 58 | init(colorModel: ColorModel, shouldFetchNextBatch: Bool) { 59 | self.colorModel = colorModel 60 | self.shouldFetchNextBatch = shouldFetchNextBatch 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Landing/LandingView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LandingView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct LandingView: View { 11 | @ScaledMetric private var buttonPadding: CGFloat = 8.0 12 | @ScaledMetric private var inverseHorizontalPadding: CGFloat = 8.0 13 | 14 | private let buttonMinHeight: CGFloat = 54.0 15 | 16 | @Bindable var viewModel: LandingViewModel 17 | 18 | @State private var isAuthenticated: Bool = false 19 | @State private var username: String = .empty 20 | @State private var pulseColor: Color = .accentColor 21 | 22 | var body: some View { 23 | VStack(alignment: .center, spacing: 24.0) { 24 | signInOutButton() 25 | pulseButton() 26 | infiniteCardsButton() 27 | colorWizardButton() 28 | } 29 | .padding([.leading, .trailing], max(56.0 - self.inverseHorizontalPadding, 4.0)) 30 | .navigationBarHidden(true) 31 | .onReceive(self.viewModel.isAuthenticated.receive(on: .main)) { 32 | self.isAuthenticated = $0 33 | } 34 | .onReceive(self.viewModel.username.receive(on: .main)) { 35 | self.username = $0 36 | } 37 | .onReceive(self.viewModel.pulseColor.receive(on: .main), withAnimation: .easeInOut) { 38 | self.pulseColor = $0.asColor() 39 | } 40 | } 41 | 42 | private func signInOutButton() -> some View { 43 | Button(action: self.viewModel.signInOrOut) { 44 | Text(self.isAuthenticated ? "Sign Out, \(self.username)" : "Sign In") 45 | .multilineTextAlignment(.center) 46 | .lineLimit(nil) 47 | .padding(self.buttonPadding) 48 | .frame(maxWidth: .infinity, minHeight: self.buttonMinHeight) 49 | .contentShape(Rectangle()) 50 | } 51 | .buttonStyle(.brightBorderedButton) 52 | .busyOverlay() 53 | .clipShape(RoundedRectangle(cornerRadius: 16.0, style: .continuous)) 54 | } 55 | 56 | private func pulseButton() -> some View { 57 | Button(action: self.viewModel.pulse) { 58 | Text("Pulse") 59 | .padding(self.buttonPadding) 60 | .frame(maxWidth: .infinity, minHeight: self.buttonMinHeight) 61 | .contentShape(Rectangle()) 62 | } 63 | .buttonStyle(.brightBorderedButton(color: self.pulseColor)) 64 | } 65 | 66 | private func infiniteCardsButton() -> some View { 67 | Button(action: self.viewModel.infiniteCards) { 68 | Text("Infinite Cards") 69 | .lineLimit(1) 70 | .minimumScaleFactor(0.75) 71 | .padding(self.buttonPadding) 72 | .frame(maxWidth: .infinity, minHeight: self.buttonMinHeight) 73 | .contentShape(Rectangle()) 74 | } 75 | .buttonStyle(.brightBorderedButton) 76 | } 77 | 78 | private func colorWizardButton() -> some View { 79 | Button(action: self.viewModel.colorWizard) { 80 | Text("Color Wizard") 81 | .lineLimit(1) 82 | .minimumScaleFactor(0.75) 83 | .padding(self.buttonPadding) 84 | .frame(maxWidth: .infinity, minHeight: self.buttonMinHeight) 85 | .contentShape(Rectangle()) 86 | } 87 | .buttonStyle(.brightBorderedButton) 88 | } 89 | } 90 | 91 | #if DEBUG 92 | struct LandingView_Previews: PreviewProvider { 93 | static let appAssembler = AppAssembler() 94 | static let viewModel = appAssembler.resolver.resolved(LandingViewModel.self) 95 | 96 | static var previews: some View { 97 | Group { 98 | LandingView(viewModel: viewModel) 99 | } 100 | } 101 | } 102 | #endif 103 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Landing/LandingViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LandingViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | protocol LandingViewModelDelegate: AnyObject { 13 | func landingViewModelDidTapPulse(_ source: LandingViewModel) 14 | func landingViewModelDidTapSignIn(_ source: LandingViewModel) 15 | func landingViewModelDidTapInfiniteCards(_ source: LandingViewModel) 16 | func landingViewModelDidTapColorWizard(_ source: LandingViewModel) 17 | } 18 | 19 | @Observable 20 | class LandingViewModel: ViewModel { 21 | private let alertService: AlertServiceProtocol 22 | private let authenticationService: AuthenticationServiceProtocol 23 | private let colorService: ColorServiceProtocol 24 | 25 | private weak var delegate: LandingViewModelDelegate? 26 | 27 | var isAuthenticated: AnyPublisher { self.authenticationService.isAuthenticated } 28 | var username: AnyPublisher { 29 | self.authenticationService.user.map { $0?.username ?? .empty }.eraseToAnyPublisher() 30 | } 31 | let pulseColor: AnyPublisher 32 | 33 | let pulse: PassthroughSubject = PassthroughSubject() 34 | let signInOrOut: PassthroughSubject = PassthroughSubject() 35 | let infiniteCards: PassthroughSubject = PassthroughSubject() 36 | let colorWizard: PassthroughSubject = PassthroughSubject() 37 | 38 | private var cancelBag: CancelBag! 39 | 40 | init(alertService: AlertServiceProtocol, authenticationService: AuthenticationServiceProtocol, colorService: ColorServiceProtocol) { 41 | self.alertService = alertService 42 | self.authenticationService = authenticationService 43 | self.colorService = colorService 44 | 45 | self.pulseColor = self.colorService.generateColors().share(replay: 1).eraseToAnyPublisher() 46 | } 47 | 48 | func setup(delegate: LandingViewModelDelegate) -> Self { 49 | self.delegate = delegate 50 | bind() 51 | return self 52 | } 53 | 54 | private func bind() { 55 | self.cancelBag = CancelBag() 56 | 57 | let alertService = self.alertService 58 | let authenticationService = self.authenticationService 59 | 60 | self.pulse 61 | .sink(receiveValue: { [weak self] in 62 | guard let self = self else { return } 63 | self.delegate?.landingViewModelDidTapPulse(self) 64 | }) 65 | .store(in: &self.cancelBag) 66 | 67 | self.signInOrOut 68 | .withLatestFrom(self.isAuthenticated) 69 | .sink { [weak self] isAuthenticated in 70 | guard let self = self else { return } 71 | if isAuthenticated { 72 | alertService.present( 73 | title: "Sign Out", 74 | message: "Are you sure you want to sign out?", 75 | primaryButton: .destructive("Yes, sign out") { 76 | authenticationService.signOutAsync() 77 | }, 78 | secondaryButton: .cancel()) 79 | } else { 80 | self.delegate?.landingViewModelDidTapSignIn(self) 81 | } 82 | } 83 | .store(in: &self.cancelBag) 84 | 85 | self.infiniteCards 86 | .sink(receiveValue: { [weak self] in 87 | guard let self = self else { return } 88 | self.delegate?.landingViewModelDidTapInfiniteCards(self) 89 | }) 90 | .store(in: &self.cancelBag) 91 | 92 | self.colorWizard 93 | .sink(receiveValue: { [weak self] in 94 | guard let self = self else { return } 95 | self.delegate?.landingViewModelDidTapColorWizard(self) 96 | }) 97 | .store(in: &self.cancelBag) 98 | } 99 | } 100 | 101 | extension LandingViewModel { 102 | enum AuthAction { 103 | case signIn 104 | case willSignOut 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Pulse/PulseView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulseView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/16/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct PulseView: View { 11 | @Bindable var viewModel: PulseViewModel 12 | 13 | @State private var title: String = .empty 14 | 15 | var body: some View { 16 | ZStack { 17 | ForEach(self.viewModel.colors) { item in 18 | PulseCircle(viewModel: item) 19 | .frame( 20 | width: max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height), 21 | height: max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height) 22 | ) 23 | } 24 | } 25 | .navigationTitle(self.title) 26 | .navigationBarTitleDisplayMode(.inline) 27 | .overlay(.thinMaterial) 28 | .onReceive(self.viewModel.title.receive(on: .main)) { 29 | self.title = $0 30 | } 31 | } 32 | } 33 | 34 | extension PulseView { 35 | struct PulseCircle: View { 36 | @Bindable var viewModel: PulseViewModel.ColorItem 37 | 38 | var body: some View { 39 | Circle() 40 | .fill(Color.white) 41 | .colorMultiply(self.viewModel.color) 42 | .opacity(self.viewModel.opacity) 43 | .animation(.easeIn, value: self.viewModel.opacity) 44 | .onAppear { 45 | withAnimation { 46 | self.viewModel.opacity = 1.0 47 | } 48 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.65) { 49 | withAnimation { 50 | self.viewModel.opacity = 0.0 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Pulse/PulseViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PulseViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/16/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import SwiftUI 11 | 12 | protocol PulseViewModelDelegate: AnyObject {} 13 | 14 | @Observable 15 | class PulseViewModel: ViewModel { 16 | private let authenticationService: AuthenticationServiceProtocol 17 | private let colorService: ColorServiceProtocol 18 | private weak var delegate: PulseViewModelDelegate? 19 | 20 | private(set) var colors: [ColorItem] = [] 21 | 22 | var title: AnyPublisher { 23 | self.authenticationService.user 24 | .map { 25 | if let username = $0?.username { 26 | return "Welcome, \(username)" 27 | } 28 | return "Hello, mysterious stranger" 29 | } 30 | .eraseToAnyPublisher() 31 | } 32 | 33 | private var cancelBag: CancelBag! 34 | 35 | init(authenticationService: AuthenticationServiceProtocol, colorService: ColorServiceProtocol) { 36 | self.authenticationService = authenticationService 37 | self.colorService = colorService 38 | } 39 | 40 | func setup(delegate: PulseViewModelDelegate) -> Self { 41 | self.delegate = delegate 42 | bind() 43 | return self 44 | } 45 | 46 | private func bind() { 47 | self.cancelBag = CancelBag() 48 | 49 | self.colorService.generateColors() 50 | .receive(on: .main) 51 | .sink(receiveValue: { [weak self] in 52 | guard let self = self else { return } 53 | self.colors.insert(ColorItem(color: $0), at: 0) 54 | if self.colors.count > 3 { 55 | let _ = self.colors.popLast() 56 | } 57 | }) 58 | .store(in: &self.cancelBag) 59 | } 60 | } 61 | 62 | extension PulseViewModel { 63 | @Observable 64 | class ColorItem: ViewModel { 65 | let id: String = UUID().uuidString 66 | let color: Color 67 | var opacity: Double = 0.0 68 | 69 | init(color model: ColorModel) { 70 | switch model { 71 | case .blue: self.color = .blue 72 | case .green: self.color = .green 73 | case .orange: self.color = .orange 74 | case .pink: self.color = .pink 75 | case .purple: self.color = .purple 76 | case .red: self.color = .red 77 | case .white: self.color = .white 78 | case .yellow: self.color = .yellow 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/SignIn/SignInView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | 10 | struct SignInView: View { 11 | @ScaledMetric private var buttonFontSize: CGFloat = 18.0 12 | @ScaledMetric private var inverseCardPadding: CGFloat = 16.0 13 | 14 | @Bindable var viewModel: SignInViewModel 15 | 16 | @State private var showCard: Bool = false 17 | @State private var signInDisabled: Bool = true 18 | 19 | @FocusState private var focusState: FocusField? 20 | enum FocusField { 21 | case username 22 | case password 23 | } 24 | 25 | var body: some View { 26 | HStack { 27 | if self.showCard { 28 | CardView(color: Color.systemBackground, cornerRadius: .large) { 29 | VStack { 30 | VStack(alignment: .leading) { 31 | Text("User Name") 32 | .padding(EdgeInsets(horizontal: 8.0, vertical: 0.0)) 33 | TextField("User Name", text: self.$viewModel.username, prompt: nil) 34 | .focused(self.$focusState, equals: .username) 35 | .padding() 36 | .background(RoundedRectangle(cornerRadius: 8.0).stroke(Color.systemGray)) 37 | .contentShape(Rectangle()) 38 | .onTapGesture { 39 | if self.focusState != .username { 40 | self.focusState = .username 41 | } 42 | } 43 | Text("Password").padding(.top) 44 | .padding(EdgeInsets(horizontal: 8.0, vertical: 0.0)) 45 | SecureField("Password", text: self.$viewModel.password, prompt: nil) 46 | .focused(self.$focusState, equals: .password) 47 | .padding() 48 | .background(RoundedRectangle(cornerRadius: 8.0).stroke(Color.systemGray)) 49 | .contentShape(Rectangle()) 50 | .onTapGesture { 51 | if self.focusState != .password { 52 | self.focusState = .password 53 | } 54 | } 55 | } 56 | .padding(16.0) 57 | .onSubmit { 58 | switch self.focusState { 59 | case .none: break 60 | case .username: self.focusState = .password 61 | case .password: self.focusState = nil 62 | } 63 | } 64 | 65 | HStack { 66 | Button(action: self.viewModel.cancel) { 67 | Text("Cancel") 68 | .lineLimit(1) 69 | .minimumScaleFactor(0.75) 70 | .font(.system(size: self.buttonFontSize)) 71 | .bold() 72 | .frame(maxWidth: .infinity, minHeight: 48.0, idealHeight: 48.0, maxHeight: 48.0) 73 | .contentShape(Rectangle()) 74 | } 75 | 76 | Button(action: self.viewModel.signIn) { 77 | Text("Sign In") 78 | .lineLimit(1) 79 | .minimumScaleFactor(0.75) 80 | .font(.system(size: self.buttonFontSize)) 81 | .frame(maxWidth: .infinity, minHeight: 48.0) 82 | .contentShape(Rectangle()) 83 | } 84 | .disabled(self.signInDisabled) 85 | 86 | } 87 | .padding(.bottom) 88 | } 89 | } 90 | .fixedSize(horizontal: false, vertical: true) 91 | .clipped() 92 | .shadow(radius: 3.0) 93 | .padding(max(52.0 - self.inverseCardPadding, 4.0)) 94 | .transition(.scale(scale: 0.0)) 95 | } 96 | } 97 | .frame(maxWidth: .infinity, maxHeight: .infinity) 98 | .background(.ultraThinMaterial) 99 | .onAppear { 100 | withAnimation(.spring(response: 0.325, dampingFraction: 0.825, blendDuration: 0.2)) { 101 | self.showCard = true 102 | } 103 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 104 | self.focusState = .username 105 | } 106 | } 107 | .onReceive(self.viewModel.canSignIn) { 108 | self.signInDisabled = !$0 109 | } 110 | } 111 | } 112 | 113 | #if DEBUG 114 | struct SignInView_Previews: PreviewProvider { 115 | static let appAssembler = AppAssembler() 116 | static let viewModel = appAssembler.resolver.resolved(SignInViewModel.self) 117 | 118 | static var previews: some View { 119 | Group { 120 | SignInView(viewModel: viewModel) 121 | .edgesIgnoringSafeArea(.all) 122 | } 123 | } 124 | } 125 | #endif 126 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/SignIn/SignInViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SignInViewModel.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import CombineExt 11 | 12 | protocol SignInViewModelDelegate: AnyObject { 13 | func signInViewModelDidCancel(_ source: SignInViewModel) 14 | func signInViewModelDidSignIn(_ source: SignInViewModel) 15 | } 16 | 17 | @Observable 18 | class SignInViewModel: ViewModel { 19 | private let authenticationService: AuthenticationServiceProtocol 20 | 21 | private weak var delegate: SignInViewModelDelegate? 22 | 23 | var thing: String = .empty 24 | 25 | var username: String = .empty { 26 | didSet { self.username$.send(self.username) } 27 | } 28 | private let username$: CurrentValueSubject = CurrentValueSubject(.empty) 29 | 30 | var password: String = .empty { 31 | didSet { self.password$.send(self.password) } 32 | } 33 | private let password$: CurrentValueSubject = CurrentValueSubject(.empty) 34 | 35 | let cancel: PassthroughSubject = PassthroughSubject() 36 | let signIn: PassthroughSubject = PassthroughSubject() 37 | 38 | private(set) var canSignIn: AnyPublisher! 39 | 40 | private var cancelBag: CancelBag! 41 | 42 | init(authenticationService: AuthenticationServiceProtocol) { 43 | self.authenticationService = authenticationService 44 | 45 | self.canSignIn = [ 46 | self.username$, 47 | self.password$, 48 | ] 49 | .combineLatest() 50 | .map { 51 | $0.allSatisfy { !$0.isEmpty } 52 | } 53 | .eraseToAnyPublisher() 54 | } 55 | 56 | func setup(delegate: SignInViewModelDelegate) -> Self { 57 | self.delegate = delegate 58 | bind() 59 | return self 60 | } 61 | 62 | private func bind() { 63 | self.cancelBag = CancelBag() 64 | 65 | let authenticationService = self.authenticationService 66 | 67 | self.cancel 68 | .sink(receiveValue: { [weak self] in 69 | guard let self = self else { return } 70 | self.endEditing() 71 | self.delegate?.signInViewModelDidCancel(self) 72 | }) 73 | .store(in: &self.cancelBag) 74 | 75 | self.signIn 76 | .withLatestFrom(self.username$, self.password$) 77 | .setFailureType(to: Error.self) 78 | .flatMapLatest { username, password -> AnyPublisher in 79 | authenticationService 80 | .signIn(username: username, password: password) 81 | } 82 | .sink(receiveValue: { [weak self] user in 83 | guard let self = self else { return } 84 | self.delegate?.signInViewModelDidSignIn(self) 85 | }) 86 | .store(in: &self.cancelBag) 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Styles/ButtonStyles/BrightBorderedButtonStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BrightBorderedButtonStyle.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 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 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Styles/TextStyles/ButtonTextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ButtonTextStyle.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 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 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Styles/TextStyles/TextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextStyle.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 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 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Views/CardView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CardView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 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 | struct CardView_Previews: PreviewProvider { 55 | static var previews: some View { 56 | return CardView( 57 | color: Color.green, 58 | cornerRadius: .medium) { 59 | Text("Card contents.") 60 | .padding() 61 | .background(Color.orange) 62 | } 63 | .clipped() 64 | .shadow(radius: 10) 65 | .padding() 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Views/Modifiers/AlertManager+Environment.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertManager+Environment.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 6/24/22. 6 | // 7 | 8 | import SwiftUI 9 | 10 | private struct AlertManagerKey: EnvironmentKey { 11 | static let defaultValue: AlertManager = AlertManager() 12 | } 13 | 14 | extension EnvironmentValues { 15 | public var alertManager: AlertManager { 16 | get { self[AlertManagerKey.self] } 17 | set { self[AlertManagerKey.self] = newValue } 18 | } 19 | } 20 | 21 | extension View { 22 | @inlinable 23 | func alertManager(_ alertManager: AlertManager) -> some View { 24 | environment(\.alertManager, alertManager) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUI/UI/Views/VisualEffectView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VisualEffectView.swift 3 | // MVVM.Demo.SwiftUI 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import SwiftUI 9 | import UIKit 10 | 11 | struct VisualEffectView: UIViewRepresentable { 12 | let effect: UIVisualEffect? 13 | func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { UIVisualEffectView() } 14 | func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = self.effect } 15 | } 16 | 17 | struct ProgressiveVisualEffectView: UIViewRepresentable { 18 | let effect: UIVisualEffect 19 | let intensity: CGFloat 20 | 21 | func makeUIView(context: UIViewRepresentableContext) -> UIVisualEffectView { 22 | UIProgressiveVisualEffectView(effect: self.effect, intensity: self.intensity) 23 | } 24 | 25 | func updateUIView(_ uiView: UIVisualEffectView, context: UIViewRepresentableContext) { uiView.effect = self.effect } 26 | } 27 | 28 | final class UIProgressiveVisualEffectView: UIVisualEffectView { 29 | private let theEffect: UIVisualEffect 30 | private let customIntensity: CGFloat 31 | private var animator: UIViewPropertyAnimator? 32 | 33 | /// Create visual effect view with given effect and its intensity 34 | /// 35 | /// - Parameters: 36 | /// - effect: visual effect, eg UIBlurEffect(style: .dark) 37 | /// - intensity: custom intensity from 0.0 (no effect) to 1.0 (full effect) using linear scale 38 | init(effect: UIVisualEffect, intensity: CGFloat) { 39 | self.theEffect = effect 40 | self.customIntensity = intensity 41 | super.init(effect: nil) 42 | } 43 | 44 | required init?(coder aDecoder: NSCoder) { nil } 45 | 46 | deinit { 47 | self.animator?.stopAnimation(true) 48 | } 49 | 50 | override func draw(_ rect: CGRect) { 51 | super.draw(rect) 52 | effect = nil 53 | self.animator?.stopAnimation(true) 54 | self.animator = UIViewPropertyAnimator(duration: 1, curve: .linear) { [unowned self] in 55 | self.effect = self.theEffect 56 | } 57 | self.animator?.fractionComplete = self.customIntensity 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/MVVM_Demo_SwiftUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVM_Demo_SwiftUITests.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import XCTest 9 | @testable import MVVM_Demo_SwiftUI 10 | 11 | class MVVM_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 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/Services/AlertService/AlertService+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertService+Tests.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 3/14/22. 6 | // 7 | 8 | import XCTest 9 | @testable import MVVM_Demo_SwiftUI 10 | 11 | class AlertServiceTest: XCTestCase { 12 | var alertManager = AlertManager() 13 | var subject: AlertService! 14 | 15 | override func setUp() async throws { 16 | try await super.setUp() 17 | self.subject = AlertService(alertManager: self.alertManager) 18 | } 19 | } 20 | 21 | class AlertService_when_buildAlert_title_message_dismissButton_and_dismissButton_is_nil_is_called: AlertServiceTest { 22 | 23 | var actualAlertPackage: AlertService.AlertPackage! 24 | 25 | var expectedTitle: String! 26 | var expectedMessage: String! 27 | 28 | override func setUp() async throws { 29 | try await super.setUp() 30 | 31 | self.expectedTitle = "test.title" 32 | self.expectedMessage = "test.message" 33 | 34 | var iterator = self.alertManager.alert$ 35 | .compactMap { $0 } 36 | .values 37 | .makeAsyncIterator() 38 | 39 | self.subject.present( 40 | title: self.expectedTitle, 41 | message: self.expectedMessage, 42 | dismissButton: nil) 43 | 44 | self.actualAlertPackage = await iterator.next() 45 | } 46 | 47 | func test_then_package_title_is_expected() { 48 | XCTAssertEqual(self.actualAlertPackage.title, self.expectedTitle) 49 | } 50 | 51 | func test_then_package_message_is_expected() { 52 | XCTAssertEqual(self.actualAlertPackage.message, self.expectedMessage) 53 | } 54 | 55 | func test_then_primaryButton_role_is_cancel() { 56 | let isCancelType: Bool 57 | 58 | switch self.actualAlertPackage.primaryButton.role { 59 | case .cancel: 60 | isCancelType = true 61 | default: 62 | isCancelType = false 63 | } 64 | 65 | XCTAssertTrue(isCancelType) 66 | } 67 | 68 | func test_then_primaryButton_title_is_Cancel() { 69 | XCTAssertEqual(self.actualAlertPackage.primaryButton.title, "Cancel") 70 | } 71 | 72 | func test_then_primaryButton_action_is_nil() { 73 | XCTAssertNil(self.actualAlertPackage.primaryButton.action) 74 | } 75 | } 76 | 77 | class AlertService_when_buildAlert_title_message_dismissButton_and_dismissButton_is_not_nil: AlertServiceTest { 78 | 79 | var actualAlertPackage: AlertService.AlertPackage! 80 | 81 | var expectedTitle: String! 82 | var expectedMessage: String! 83 | var expectedButton: AlertService.AlertPackage.Button! 84 | 85 | override func setUp() async throws { 86 | try await super.setUp() 87 | 88 | self.expectedTitle = "test.title" 89 | self.expectedMessage = "test.message" 90 | self.expectedButton = AlertService.AlertPackage.Button( 91 | role: .default, 92 | title: "button.title", 93 | action: nil) 94 | 95 | var iterator = self.alertManager.alert$ 96 | .compactMap { $0 } 97 | .values 98 | .makeAsyncIterator() 99 | 100 | self.subject.present( 101 | title: self.expectedTitle, 102 | message: self.expectedMessage, 103 | dismissButton: self.expectedButton) 104 | 105 | self.actualAlertPackage = await iterator.next() 106 | } 107 | 108 | func test_then_package_title_is_expected() { 109 | XCTAssertEqual(self.actualAlertPackage.title, self.expectedTitle) 110 | } 111 | 112 | func test_then_package_message_is_expected() { 113 | XCTAssertEqual(self.actualAlertPackage.message, self.expectedMessage) 114 | } 115 | 116 | func test_then_primaryButton_role_is_expected() { 117 | XCTAssertEqual(self.actualAlertPackage.primaryButton.role, self.expectedButton.role) 118 | } 119 | 120 | func test_then_primaryButton_title_is_expected() { 121 | XCTAssertEqual(self.actualAlertPackage.primaryButton.title, self.expectedButton.title) 122 | } 123 | 124 | func test_then_primaryButton_action_is_expected() { 125 | XCTAssertEqual(self.actualAlertPackage.primaryButton.action == nil, self.expectedButton.action == nil) 126 | } 127 | } 128 | 129 | class AlertService_when_buildAlert_title_message_primaryButton_secondaryButton_is_called: AlertServiceTest { 130 | 131 | var actualAlertPackage: AlertService.AlertPackage! 132 | 133 | var expectedTitle: String! 134 | var expectedMessage: String! 135 | var expectedPrimaryButton: AlertService.AlertPackage.Button! 136 | var expectedSecondaryButton: AlertService.AlertPackage.Button! 137 | 138 | override func setUp() async throws { 139 | try await super.setUp() 140 | 141 | self.expectedTitle = "test.title" 142 | self.expectedMessage = "test.message" 143 | 144 | self.expectedPrimaryButton = AlertService.AlertPackage.Button( 145 | role: .cancel, 146 | title: "button.title.primary", 147 | action: nil) 148 | 149 | self.expectedSecondaryButton = AlertService.AlertPackage.Button( 150 | role: .default, 151 | title: "button.title.secondary", 152 | action: nil) 153 | 154 | var iterator = self.alertManager.alert$ 155 | .compactMap { $0 } 156 | .values 157 | .makeAsyncIterator() 158 | 159 | self.subject.present( 160 | title: self.expectedTitle, 161 | message: self.expectedMessage, 162 | primaryButton: self.expectedPrimaryButton, 163 | secondaryButton: self.expectedSecondaryButton) 164 | 165 | self.actualAlertPackage = await iterator.next() 166 | } 167 | 168 | func test_then_package_title_is_expected() { 169 | XCTAssertEqual(self.actualAlertPackage.title, self.expectedTitle) 170 | } 171 | 172 | func test_then_package_message_is_expected() { 173 | XCTAssertEqual(self.actualAlertPackage.message, self.expectedMessage) 174 | } 175 | 176 | func test_then_primaryButton_role_is_expected() { 177 | XCTAssertEqual(self.actualAlertPackage.primaryButton.role, self.expectedPrimaryButton.role) 178 | } 179 | 180 | func test_then_primaryButton_title_is_expected() { 181 | XCTAssertEqual(self.actualAlertPackage.primaryButton.title, self.expectedPrimaryButton.title) 182 | } 183 | 184 | func test_then_primaryButton_action_is_expected() { 185 | XCTAssertEqual(self.actualAlertPackage.primaryButton.action == nil, self.expectedPrimaryButton.action == nil) 186 | } 187 | 188 | func test_then_secondaryButton_role_is_expected() { 189 | XCTAssertEqual(self.actualAlertPackage.secondaryButton?.role, self.expectedSecondaryButton.role) 190 | } 191 | 192 | func test_then_secondaryButton_title_is_expected() { 193 | XCTAssertEqual(self.actualAlertPackage.secondaryButton?.title, self.expectedSecondaryButton.title) 194 | } 195 | 196 | func test_then_secondaryButton_action_is_expected() { 197 | XCTAssertEqual(self.actualAlertPackage.secondaryButton?.action == nil, self.expectedSecondaryButton.action == nil) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/Services/AlertService/AlertService.AlertPackage.Button+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AlertService.AlertPackage.Button+Tests.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 3/14/22. 6 | // 7 | 8 | import XCTest 9 | @testable import MVVM_Demo_SwiftUI 10 | 11 | class AlertServiceAlertPackageButtonTest: XCTestCase { 12 | var subject: AlertService.AlertPackage.Button! 13 | } 14 | 15 | class AlertServiceAlertPackageButton_when_constructed: AlertServiceAlertPackageButtonTest { 16 | 17 | var expectedRole: AlertService.AlertPackage.Button.Role! 18 | var expectedTitle: String! 19 | var expectedAction: (() -> Void)? 20 | 21 | override func setUp() { 22 | super.setUp() 23 | 24 | self.expectedRole = .default 25 | self.expectedTitle = "test.title" 26 | self.expectedAction = nil 27 | 28 | self.subject = AlertService.AlertPackage.Button( 29 | role: self.expectedRole, 30 | title: self.expectedTitle, 31 | action: self.expectedAction) 32 | } 33 | 34 | func test_then_role_is_expected() { 35 | XCTAssertEqual(self.subject.role, self.expectedRole) 36 | } 37 | 38 | func test_then_title_is_expected() { 39 | XCTAssertEqual(self.subject.title, self.expectedTitle) 40 | } 41 | 42 | func test_then_action_is_expected() { 43 | XCTAssertEqual(self.subject.action == nil, self.expectedAction == nil) 44 | } 45 | } 46 | 47 | class AlertServiceAlertPackageButton_when_default_button_is_created: AlertServiceAlertPackageButtonTest { 48 | var expectedTitle: String! 49 | var expectedAction: (() -> Void)? 50 | 51 | override func setUp() { 52 | super.setUp() 53 | 54 | self.expectedTitle = "test.title" 55 | self.expectedAction = nil 56 | 57 | self.subject = .default( 58 | self.expectedTitle, 59 | action: self.expectedAction) 60 | } 61 | 62 | func test_then_role_is_default() { 63 | XCTAssertEqual(self.subject.role, .default) 64 | } 65 | 66 | func test_then_title_is_expected() { 67 | XCTAssertEqual(self.subject.title, self.expectedTitle) 68 | } 69 | 70 | func test_then_action_is_expected() { 71 | XCTAssertEqual(self.subject.action == nil, self.expectedAction == nil) 72 | } 73 | } 74 | 75 | class AlertServiceAlertPackageButton_when_cancel_button_is_created: AlertServiceAlertPackageButtonTest { 76 | var expectedTitle: String! 77 | var expectedAction: (() -> Void)? 78 | 79 | override func setUp() { 80 | super.setUp() 81 | 82 | self.expectedTitle = "test.title" 83 | self.expectedAction = nil 84 | 85 | self.subject = .cancel( 86 | self.expectedTitle, 87 | action: self.expectedAction) 88 | } 89 | 90 | func test_then_role_is_cancel() { 91 | XCTAssertEqual(self.subject.role, .cancel) 92 | } 93 | 94 | func test_then_title_is_expected() { 95 | XCTAssertEqual(self.subject.title, self.expectedTitle) 96 | } 97 | 98 | func test_then_action_is_expected() { 99 | XCTAssertEqual(self.subject.action == nil, self.expectedAction == nil) 100 | } 101 | } 102 | 103 | class AlertServiceAlertPackageButton_when_destructive_button_is_created: AlertServiceAlertPackageButtonTest { 104 | var expectedTitle: String! 105 | var expectedAction: (() -> Void)? 106 | 107 | override func setUp() { 108 | super.setUp() 109 | 110 | self.expectedTitle = "test.title" 111 | self.expectedAction = nil 112 | 113 | self.subject = .destructive( 114 | self.expectedTitle, 115 | action: self.expectedAction) 116 | } 117 | 118 | func test_then_role_is_cancel() { 119 | XCTAssertEqual(self.subject.role, .destructive) 120 | } 121 | 122 | func test_then_title_is_expected() { 123 | XCTAssertEqual(self.subject.title, self.expectedTitle) 124 | } 125 | 126 | func test_then_action_is_expected() { 127 | XCTAssertEqual(self.subject.action == nil, self.expectedAction == nil) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/Services/AuthenticationService+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AuthenticationService+Tests.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 12/23/21. 6 | // 7 | 8 | import Foundation 9 | @testable import MVVM_Demo_SwiftUI 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | import BusyIndicator 14 | 15 | class AuthenticationServiceTest: XCTestCase { 16 | var subject: AuthenticationService! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | self.subject = AuthenticationService(busyIndicatorService: BusyIndicatorService()) 21 | } 22 | } 23 | 24 | class AuthenticationService_when_initialized: AuthenticationServiceTest { 25 | func test_then_user_is_nil() async { 26 | let results = await self.subject.user.awaitResults(1) 27 | XCTAssertEqual(results?[0], .success(nil)) 28 | } 29 | 30 | func test_then_isAuthenticated_is_false() async { 31 | let results = await self.subject.isAuthenticated.awaitResults(1) 32 | XCTAssertEqual(results?[0], .success(false)) 33 | } 34 | } 35 | 36 | class AuthenticationService_when_a_user_signs_in: AuthenticationServiceTest { 37 | var expectedUsername: String! 38 | var expectedPassword: String! 39 | 40 | var signinPublisher: AnyPublisher! 41 | 42 | override func setUp() { 43 | super.setUp() 44 | 45 | self.expectedUsername = "test.user.name" 46 | self.expectedPassword = "test.password" 47 | 48 | self.signinPublisher = self.subject 49 | .signIn(username: self.expectedUsername, password: self.expectedPassword) 50 | .share() 51 | .eraseToAnyPublisher() 52 | } 53 | 54 | func test_then_emitted_user_is_not_nil() async { 55 | let results = await self.signinPublisher.awaitResults(1) 56 | XCTAssertNotNil(results?.first?.value) 57 | } 58 | 59 | func test_then_emitted_user_has_expected_username() async { 60 | let results = await self.signinPublisher.awaitResults(1) 61 | XCTAssertEqual(results?.first?.value?.username, self.expectedUsername) 62 | } 63 | 64 | func test_then_emitted_user_has_expected_password() async { 65 | let results = await self.signinPublisher.awaitResults(1) 66 | XCTAssertEqual(results?.first?.value?.password, self.expectedPassword) 67 | } 68 | 69 | func test_then_user_is_not_nil() async { 70 | let _ = await self.signinPublisher.awaitResults(1) 71 | let results = await self.subject.user.awaitResults(1) 72 | XCTAssertNotEqual(results?[0], .success(nil)) 73 | } 74 | 75 | func test_then_isAuthenticated_is_true() async { 76 | let _ = await self.signinPublisher.awaitResults(1) 77 | let results = await self.subject.isAuthenticated.awaitResults(1) 78 | XCTAssertEqual(results?[0], .success(true)) 79 | } 80 | } 81 | 82 | class AuthenticationService_when_a_user_signs_out_after_signing_in: AuthenticationServiceTest { 83 | var expectedUsername: String! 84 | var expectedPassword: String! 85 | 86 | var signInPublisher: AnyPublisher! 87 | var signOutPublisher: AnyPublisher! 88 | 89 | override func setUp() { 90 | super.setUp() 91 | 92 | self.expectedUsername = "test.user.name" 93 | self.expectedPassword = "test.password" 94 | 95 | self.signInPublisher = self.subject 96 | .signIn(username: self.expectedUsername, password: self.expectedPassword) 97 | 98 | self.signOutPublisher = self.subject 99 | .signOut() 100 | } 101 | 102 | func test_then_success_is_emitted() async { 103 | let _ = await self.signInPublisher.awaitResults(1) 104 | let results = await self.signOutPublisher.awaitResults(1) 105 | XCTAssertTrue(results?[0].isSuccess ?? false) 106 | } 107 | 108 | func test_then_user_emits_nil() async { 109 | let _ = await self.signInPublisher.awaitResults(1) 110 | let _ = await self.signOutPublisher.awaitResults(1) 111 | 112 | let results = await self.subject.user 113 | .awaitResults(1) 114 | 115 | XCTAssertEqual(results?.first, .success(nil)) 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/Services/ColorService+Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ColorService+Tests.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 2/28/22. 6 | // 7 | 8 | import Foundation 9 | @testable import MVVM_Demo_SwiftUI 10 | import XCTest 11 | import Combine 12 | import CombineExt 13 | import SwiftUI 14 | 15 | class ColorServiceTest: XCTestCase { 16 | var subject: ColorService! 17 | 18 | override func setUp() { 19 | super.setUp() 20 | self.subject = ColorService() 21 | } 22 | } 23 | 24 | class ColorService_when_getNextColor_is_called: ColorServiceTest { 25 | func test_then_colors_are_returned_in_expected_order() { 26 | let actualColors: [ColorModel] = [ 27 | self.subject.getNextColor(), 28 | self.subject.getNextColor(), 29 | self.subject.getNextColor(), 30 | self.subject.getNextColor(), 31 | self.subject.getNextColor(), 32 | self.subject.getNextColor(), 33 | self.subject.getNextColor(), 34 | self.subject.getNextColor(), 35 | self.subject.getNextColor(), 36 | self.subject.getNextColor(), 37 | ] 38 | 39 | let expectedColors: [ColorModel] = [ 40 | .blue, 41 | .green, 42 | .orange, 43 | .pink, 44 | .purple, 45 | .red, 46 | .yellow, 47 | .blue, 48 | .green, 49 | .orange, 50 | ] 51 | 52 | XCTAssertEqual(actualColors, expectedColors) 53 | } 54 | } 55 | 56 | class ColorService_when_generateNextColor_is_called: ColorServiceTest { 57 | var subscription: AnyCancellable? 58 | var values: [ColorModel] = [] 59 | 60 | override func setUp() { 61 | super.setUp() 62 | } 63 | 64 | override func tearDown() { 65 | super.tearDown() 66 | self.subscription?.cancel() 67 | } 68 | 69 | func test_then_emitted_colors_are_expected() async { 70 | var iterator = self.subject 71 | .generateColors() 72 | .buffer(size: 10, prefetch: .keepFull, whenFull: .dropNewest) 73 | .flatMap(maxPublishers: .max(1)) { output in 74 | Just(output).eraseToAnyPublisher() 75 | } 76 | .eraseToAnyPublisher() 77 | .values.makeAsyncIterator() 78 | 79 | for _ in 0..<10 { 80 | guard let color = await iterator.next() else { 81 | return 82 | } 83 | self.values.append(color) 84 | } 85 | 86 | XCTAssertEqual(values.count, 10) 87 | 88 | let possibleColors: [ColorModel] = [ 89 | .blue, 90 | .green, 91 | .orange, 92 | .pink, 93 | .purple, 94 | .red, 95 | .yellow, 96 | ] 97 | 98 | values.forEach { color in 99 | XCTAssertTrue(possibleColors.contains(color)) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/TestExtensions/AnyPublisher+AwaitResults.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnyPublisher+AwaitResults.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 12/23/21. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import CombineExt 11 | 12 | public extension AnyPublisher { 13 | /// Buffers `count` events from the `Publisher` and returns them 14 | /// as an ordered array of `Result`. If there 15 | /// are no events to buffer, `nil` is returned. If the `Publisher` 16 | /// errors, it will be reflected in the `Result`s. 17 | @available(iOS 15.0, *) 18 | func awaitResults(_ count: Int, scheduler: DispatchQueue = DispatchQueue.main) async -> [Result]? { 19 | let publisher: AnyPublisher<[Result], Never> = self 20 | .map { 21 | Result.success($0) 22 | } 23 | .catch { 24 | CurrentValueSubject(Result.failure($0)).eraseToAnyPublisher() 25 | } 26 | .collect(count) 27 | .timeout(5.0, scheduler: scheduler) 28 | .share(replay: 1) 29 | .eraseToAnyPublisher() 30 | 31 | var iterator = publisher.values.makeAsyncIterator() 32 | return await iterator.next() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUITests/TestExtensions/Result+TestExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result+TestExtensions.swift 3 | // MVVM.Demo.SwiftUITests 4 | // 5 | // Created by Jason Lew-Rapai on 12/23/21. 6 | // 7 | 8 | import Foundation 9 | 10 | extension Result { 11 | var value: Success? { 12 | switch self { 13 | case .success(let value): return value 14 | case .failure: return nil 15 | } 16 | } 17 | 18 | var error: Failure? { 19 | switch self { 20 | case .success: return nil 21 | case .failure(let error): return error 22 | } 23 | } 24 | 25 | var isSuccess: Bool { 26 | switch self { 27 | case .success: return true 28 | case .failure: return false 29 | } 30 | } 31 | 32 | var isFailure: Bool { 33 | switch self { 34 | case .success: return false 35 | case .failure: return true 36 | } 37 | } 38 | } 39 | 40 | extension Result where Success == Void { 41 | var valueIsVoid: Bool { 42 | switch self { 43 | case .success: return true 44 | case .failure: return false 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUIUITests/MVVM_Demo_SwiftUIUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVM_Demo_SwiftUIUITests.swift 3 | // MVVM.Demo.SwiftUIUITests 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class MVVM_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 recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /MVVM.Demo.SwiftUIUITests/MVVM_Demo_SwiftUIUITestsLaunchTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MVVM_Demo_SwiftUIUITestsLaunchTests.swift 3 | // MVVM.Demo.SwiftUIUITests 4 | // 5 | // Created by Jason Lew-Rapai on 11/15/21. 6 | // 7 | 8 | import XCTest 9 | 10 | class MVVM_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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MVVM.Demo.SwiftUI 2 | 3 | # MVVM - Model View ViewModel 4 | MVVM Wiki: https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93viewmodel 5 | 6 | ## Overview 7 | ### View 8 | - UI elements 9 | - Reacts to and interprets ViewModel through bindings
 10 | - Contains no business logic 11 | - Stateless1 12 | - Holds a reference to the ViewModel 13 | 14 | ### ViewModel 15 | - Model interpretation
 16 | - Business logic
 17 | - Bindable properties 18 | - Domain model dependencies injected as Protocols
 19 | - May contain child ViewModels
 20 | - Does not know about View 21 | 22 | ### Model 23 | - Anything that provides data or state to the ViewModel 24 | - Does not know about the View or ViewModel 25 | 26 | 1 No meaningful state is stored within the view. Anything needed by the model layer is immediately sent to the ViewModel from the View. 27 | 28 | # Coordinators 29 | - Coordinators are aware of the user’s navigation context within the app 30 | - Primarily responsible for Navigation and ViewModel injection 31 | - Can contain child coordinators which are responsible for a narrowed context 32 | - Only object to have a reference to the Dependency Injection container/resolver 33 | 34 | # Dependency Injection Container 35 | - Manages references and lifetimes of registered objects
 36 | - Reduces the burden of dependency injection by defining all injections in one place
 37 | - Greatly improves the ability to unit test your code by registering contained objects as Protocols which can be substituted with mocks 38 | - Makes sharing observable data throughout the app trivial while keeping each class/service focused and independent 39 | 40 | # Looking for older iOS Examples? 41 | 42 | ## iOS 16 43 | https://github.com/jasonjrr/MVVM.Demo.SwiftUI/releases/tag/3.0.0 44 | 45 | ## iOS 14 & 15 46 | https://github.com/jasonjrr/MVVM.Demo.SwiftUI/releases/tag/2.1.0 47 | 48 | Version `2.1.0` was built on iOS 14 and solves the navigation problems most developers experiences with `NavigationView`. 49 | --------------------------------------------------------------------------------