From 7ec3de28eb603890b9f33c79df7c781b6c9bd3ab Mon Sep 17 00:00:00 2001 From: Filip Sasala <31418257+plajdo@users.noreply.github.com> Date: Fri, 14 Jun 2024 20:08:31 +0200 Subject: [PATCH] feat: Swift 6 appdebugmode - Added pulse logs --- .../project.pbxproj | 126 ++--- .../xcshareddata/swiftpm/Package.resolved | 169 +++--- .../Application/AppDelegate.swift | 39 +- .../Coordinators/AppCoordinator.swift | 11 +- .../Coordinators/Coordinator.swift | 7 +- .../Coordinators/FetchCoordinator.swift | 28 - .../Coordinators/HomeCoordinator.swift | 61 +- .../Coordinators/LoginCoordinator.swift | 28 - .../Coordinators/ProfileCoordinator.swift | 28 - .../Extensions/UIControlExtensions.swift | 8 +- .../Helpers/Constants.swift | 108 +--- .../Managers/CacheManager/CacheManager.swift | 34 -- .../CacheManager/CacheManagerType.swift | 20 - .../DependencyContainer.swift | 27 - .../DependencyContainer/SampleContainer.swift | 56 ++ .../RequestManager/RequestManager.swift | 83 ++- .../RequestManager/RequestManagerType.swift | 8 +- .../Screens/ Login/LoginViewController.swift | 100 ---- .../Screens/ Login/LoginViewModel.swift | 25 - .../ApiServerMode/ApiServerModeView.swift | 132 +++++ .../ApiServerModeViewModel.swift | 115 ++++ .../Screens/BaseHostingViewController.swift | 49 ++ .../BaseViewController.swift | 26 - .../Screens/Fetch/FetchViewController.swift | 193 ------- .../Screens/Fetch/FetchViewModel.swift | 127 ----- .../Screens/Home/HomeViewController.swift | 148 +++-- .../Screens/Home/HomeViewModel.swift | 17 +- .../Screens/LoginView.swift | 70 +++ .../Profile/ProfileViewController.swift | 119 ---- .../Screens/Profile/ProfileViewModel.swift | 54 -- .../Screens/ProfileView.swift | 61 ++ .../Views/UIButton/FetchButton.swift | 12 +- .../GoodDependencies/Package.resolved | 113 ---- .../Packages/GoodDependencies/Package.swift | 10 +- Package.resolved | 29 +- Package.swift | 25 +- .../Logging/StandardOutput.swift} | 117 +--- .../Logging/StandardOutputProcessor.swift | 76 +++ .../DebugManSessionManager.swift | 521 ++++++++++++++++++ .../PeerBrowserDelegateWrapper.swift | 60 ++ .../PeerSessionDelegateWrapper.swift | 88 +++ .../ProxySettingsProvider.swift | 106 ++++ .../Dependencies/PackageManager.swift | 131 +++++ .../Dependencies/UserProfilesProvider.swift | 20 + .../AppDebugMode/DependencyContainer.swift | 85 +++ .../Foundation/NSPredicateExtension.swift | 14 - .../Extensions/SwiftUI/ColorExtension.swift | 29 - .../UIKit/UIViewControllerExtension.swift | 16 +- .../Models/{ => APNS}/FirebaseMessaging.swift | 0 .../APNS/PushNotificationsProvider.swift | 28 + .../Models/ApiServerCollection.swift | 64 --- .../{ => ApiServerSelection}/ApiServer.swift | 4 +- .../ApiServerCollection.swift | 21 + Sources/AppDebugMode/Models/Logging/Log.swift | 94 ++++ .../DebugManConnectionState.swift | 184 +++++++ .../DebugmanSessionState.swift | 144 +++++ .../MultipeerConnectivity/JSONPacket.swift | 81 +++ .../Models/MultipeerConnectivity/Peer.swift | 78 +++ .../PeerConnectionState.swift | 37 ++ .../PeerSessionError.swift | 84 +++ .../ProxyConfiguration.swift | 13 + .../ProxyConfigurationError.swift | 90 +++ .../ProxyConfigurationState.swift | 163 ++++++ .../ProxyConfigurationTestResult.swift | 14 + .../Models/PushNotificationsProvider.swift | 22 - Sources/AppDebugMode/Models/UserProfile.swift | 15 - .../UserProfileProviding/UserProfile.swift | 40 ++ .../Providers/AppDebugModeProvider.swift | 130 ----- .../Providers/CacheProvider.swift | 192 ------- .../Providers/ConnectionsManager.swift | 94 ---- .../Providers/ProxySettingsProvider.swift | 40 -- .../Providers/UserProfilesProvider.swift | 39 -- .../AppDirectorySettingsViewModel.swift | 1 + .../ConnectedPeers/ConnectedPeersView.swift | 83 --- .../ConnectionsSettingsView.swift | 257 ++++++++- .../ProxySettings/ProxySettingsView.swift | 84 --- .../ProxySettingsViewModel.swift | 46 -- .../Views/NearbyServicesView.swift | 180 ++++++ .../Views/RequestingCertificateView.swift | 92 ++++ .../Views/RequireCertificateTrustView.swift | 87 +++ .../ConsoleLogDetailView.swift | 2 +- ...gsView.swift => ConsoleLogsListView.swift} | 65 ++- .../ConsoleLogsSettingsView.swift | 41 +- .../KeychainSettingsView.swift | 130 ----- .../KeychainSettingsViewModel.swift | 71 --- .../PushNotificationsSettingsViewModel.swift | 3 +- .../Screens/ResetAppView/ResetAppView.swift | 9 +- .../ServerPickerView.swift | 227 ++++++++ .../ServerPickerView/ServerPickerView.swift | 117 ---- .../ServerPickerViewModel.swift | 34 -- .../ServersCollectionView.swift | 91 ++- .../ServersCollectionViewModel.swift | 31 -- .../UserDefaultsSettingsView.swift | 117 ---- .../UserDefaultsSettingsViewModel.swift | 50 -- .../UserProfilesPickerView.swift | 30 +- .../UserProfilesPickerViewModel.swift | 20 +- .../AppDebugMode/Utils/AppDebugColors.swift | 21 +- .../Utils/AppDebugModeLogger.swift | 38 ++ Sources/AppDebugMode/Utils/Constants.swift | 19 +- .../Utils/DebugSeverSelector.swift | 108 ++++ .../Utils/GRHaptics/GRHapticsManager.swift | 153 +++++ .../Utils/GRHaptics/GRHapticsPattern.swift | 39 ++ Sources/AppDebugMode/Views/AppDebugView.swift | 91 +-- .../Views/Buttons/ButtonFilled.swift | 15 +- .../AppDebugModeTests/AppDebugModeTests.swift | 2 +- 105 files changed, 4440 insertions(+), 3114 deletions(-) delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/FetchCoordinator.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/LoginCoordinator.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/ProfileCoordinator.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManager.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManagerType.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/DependencyContainer.swift create mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/SampleContainer.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewController.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewModel.swift create mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeView.swift create mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeViewModel.swift create mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseHostingViewController.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseViewController/BaseViewController.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewController.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewModel.swift create mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/LoginView.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewController.swift delete mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewModel.swift create mode 100644 AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ProfileView.swift delete mode 100644 AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.resolved rename Sources/AppDebugMode/{Utils/StandardOutputService.swift => Dependencies/Logging/StandardOutput.swift} (55%) create mode 100644 Sources/AppDebugMode/Dependencies/Logging/StandardOutputProcessor.swift create mode 100644 Sources/AppDebugMode/Dependencies/MultipeerConnectivity/DebugManSessionManager.swift create mode 100644 Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerBrowserDelegateWrapper.swift create mode 100644 Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerSessionDelegateWrapper.swift create mode 100644 Sources/AppDebugMode/Dependencies/MultipeerConnectivity/ProxySettingsProvider.swift create mode 100644 Sources/AppDebugMode/Dependencies/PackageManager.swift create mode 100644 Sources/AppDebugMode/Dependencies/UserProfilesProvider.swift create mode 100644 Sources/AppDebugMode/DependencyContainer.swift delete mode 100644 Sources/AppDebugMode/Extensions/Foundation/NSPredicateExtension.swift delete mode 100644 Sources/AppDebugMode/Extensions/SwiftUI/ColorExtension.swift rename Sources/AppDebugMode/Models/{ => APNS}/FirebaseMessaging.swift (100%) create mode 100644 Sources/AppDebugMode/Models/APNS/PushNotificationsProvider.swift delete mode 100644 Sources/AppDebugMode/Models/ApiServerCollection.swift rename Sources/AppDebugMode/Models/{ => ApiServerSelection}/ApiServer.swift (76%) create mode 100644 Sources/AppDebugMode/Models/ApiServerSelection/ApiServerCollection.swift create mode 100644 Sources/AppDebugMode/Models/Logging/Log.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/DebugManConnectionState.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/DebugmanSessionState.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/JSONPacket.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/Peer.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/PeerConnectionState.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/PeerSessionError.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfiguration.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationError.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationState.swift create mode 100644 Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationTestResult.swift delete mode 100644 Sources/AppDebugMode/Models/PushNotificationsProvider.swift delete mode 100644 Sources/AppDebugMode/Models/UserProfile.swift create mode 100644 Sources/AppDebugMode/Models/UserProfileProviding/UserProfile.swift delete mode 100644 Sources/AppDebugMode/Providers/AppDebugModeProvider.swift delete mode 100644 Sources/AppDebugMode/Providers/CacheProvider.swift delete mode 100644 Sources/AppDebugMode/Providers/ConnectionsManager.swift delete mode 100644 Sources/AppDebugMode/Providers/ProxySettingsProvider.swift delete mode 100644 Sources/AppDebugMode/Providers/UserProfilesProvider.swift delete mode 100644 Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectedPeers/ConnectedPeersView.swift delete mode 100644 Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsView.swift delete mode 100644 Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsViewModel.swift create mode 100644 Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/NearbyServicesView.swift create mode 100644 Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequestingCertificateView.swift create mode 100644 Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequireCertificateTrustView.swift rename Sources/AppDebugMode/Screens/ConsoleLogsVIew/{ConsoleLogsView.swift => ConsoleLogsListView.swift} (82%) delete mode 100644 Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsView.swift delete mode 100644 Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsViewModel.swift create mode 100644 Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView.swift delete mode 100644 Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerView.swift delete mode 100644 Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerViewModel.swift delete mode 100644 Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionViewModel.swift delete mode 100644 Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsView.swift delete mode 100644 Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsViewModel.swift create mode 100644 Sources/AppDebugMode/Utils/AppDebugModeLogger.swift create mode 100644 Sources/AppDebugMode/Utils/DebugSeverSelector.swift create mode 100644 Sources/AppDebugMode/Utils/GRHaptics/GRHapticsManager.swift create mode 100644 Sources/AppDebugMode/Utils/GRHaptics/GRHapticsPattern.swift diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.pbxproj b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.pbxproj index b96f452..0d4dbd0 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.pbxproj +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ 5D73FB592B17B2E200865E4C /* AppDebugMode in Frameworks */ = {isa = PBXBuildFile; productRef = 5D73FB582B17B2E200865E4C /* AppDebugMode */; }; 5D8F1E662B8F2EDD00D84DBE /* LargeObjectResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D8F1E652B8F2EDD00D84DBE /* LargeObjectResponse.swift */; }; + 5D99DB182CBACC02001B3C91 /* BaseHostingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D99DB172CBACBF9001B3C91 /* BaseHostingViewController.swift */; }; 5DEB2AC02B17B00C0074E12A /* AppDebugMode in Frameworks */ = {isa = PBXBuildFile; productRef = 5DEB2ABF2B17B00C0074E12A /* AppDebugMode */; }; 784AB0D92ABB15E9001801FC /* InputTextFieldView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 784AB0D82ABB15E9001801FC /* InputTextFieldView.swift */; }; 785AC50C2AB2EBEE002C1BC1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC50B2AB2EBEE002C1BC1 /* AppDelegate.swift */; }; @@ -16,19 +17,13 @@ 785AC51A2AB2EBF0002C1BC1 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 785AC5182AB2EBF0002C1BC1 /* LaunchScreen.storyboard */; }; 785AC5232AB2EDDE002C1BC1 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5222AB2EDDE002C1BC1 /* Coordinator.swift */; }; 785AC5252AB2EE0D002C1BC1 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5242AB2EE0D002C1BC1 /* AppCoordinator.swift */; }; - 785AC5282AB2EE66002C1BC1 /* DependencyContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5272AB2EE66002C1BC1 /* DependencyContainer.swift */; }; + 785AC5282AB2EE66002C1BC1 /* SampleContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5272AB2EE66002C1BC1 /* SampleContainer.swift */; }; 785AC52B2AB2EEA6002C1BC1 /* RequestManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC52A2AB2EEA6002C1BC1 /* RequestManagerType.swift */; }; 785AC52D2AB2EED8002C1BC1 /* RequestManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC52C2AB2EED8002C1BC1 /* RequestManager.swift */; }; - 785AC5322AB2EF81002C1BC1 /* FetchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5312AB2EF81002C1BC1 /* FetchCoordinator.swift */; }; - 785AC5352AB2EFB0002C1BC1 /* FetchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5342AB2EFB0002C1BC1 /* FetchViewModel.swift */; }; - 785AC5372AB2F03F002C1BC1 /* FetchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5362AB2F03F002C1BC1 /* FetchViewController.swift */; }; - 785AC53B2AB2F07E002C1BC1 /* BaseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC53A2AB2F07E002C1BC1 /* BaseViewController.swift */; }; + 785AC5352AB2EFB0002C1BC1 /* ApiServerModeViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5342AB2EFB0002C1BC1 /* ApiServerModeViewModel.swift */; }; + 785AC5372AB2F03F002C1BC1 /* ApiServerModeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 785AC5362AB2F03F002C1BC1 /* ApiServerModeView.swift */; }; 7874BEF92AC1AFA6007F0234 /* AppDebugMode in Frameworks */ = {isa = PBXBuildFile; productRef = 7874BEF82AC1AFA6007F0234 /* AppDebugMode */; }; - 7874BEFC2AC1B39E007F0234 /* CacheManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7874BEFB2AC1B39E007F0234 /* CacheManager.swift */; }; - 7874BEFE2AC1B3B5007F0234 /* CacheManagerType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7874BEFD2AC1B3B5007F0234 /* CacheManagerType.swift */; }; - 7874BF012AC1B869007F0234 /* ProfileViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7874BF002AC1B869007F0234 /* ProfileViewModel.swift */; }; - 7874BF032AC1B902007F0234 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7874BF022AC1B902007F0234 /* ProfileViewController.swift */; }; - 7874BF052AC1BB8E007F0234 /* ProfileCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7874BF042AC1BB8E007F0234 /* ProfileCoordinator.swift */; }; + 7874BF032AC1B902007F0234 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7874BF022AC1B902007F0234 /* ProfileView.swift */; }; 78A589F12AB88ADA004141F6 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A589F02AB88ADA004141F6 /* Endpoint.swift */; }; 78A589F42AB88BFC004141F6 /* CarResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A589F32AB88BFC004141F6 /* CarResponse.swift */; }; 78A589F62AB88D38004141F6 /* ProductResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78A589F52AB88D38004141F6 /* ProductResponse.swift */; }; @@ -38,9 +33,7 @@ 78CDA7002ABAD2DF00CC1A82 /* HomeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA6FF2ABAD2DF00CC1A82 /* HomeViewController.swift */; }; 78CDA7022ABAD47300CC1A82 /* HomeCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA7012ABAD47300CC1A82 /* HomeCoordinator.swift */; }; 78CDA7042ABAD72300CC1A82 /* BasicButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA7032ABAD72300CC1A82 /* BasicButton.swift */; }; - 78CDA7072ABAF3AD00CC1A82 /* LoginViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA7062ABAF3AD00CC1A82 /* LoginViewModel.swift */; }; - 78CDA7092ABAF41500CC1A82 /* LoginViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA7082ABAF41500CC1A82 /* LoginViewController.swift */; }; - 78CDA70B2ABAF68500CC1A82 /* LoginCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA70A2ABAF68500CC1A82 /* LoginCoordinator.swift */; }; + 78CDA7092ABAF41500CC1A82 /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78CDA7082ABAF41500CC1A82 /* LoginView.swift */; }; 78E10E3D2AB9D9BA00934298 /* FetchButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E10E3C2AB9D9BA00934298 /* FetchButton.swift */; }; 78E10E3F2AB9DDE000934298 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78E10E3E2AB9DDE000934298 /* Constants.swift */; }; /* End PBXBuildFile section */ @@ -48,6 +41,7 @@ /* Begin PBXFileReference section */ 5D73FB5A2B17B30400865E4C /* AppDebugMode-iOS */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "AppDebugMode-iOS"; path = ..; sourceTree = ""; }; 5D8F1E652B8F2EDD00D84DBE /* LargeObjectResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeObjectResponse.swift; sourceTree = ""; }; + 5D99DB172CBACBF9001B3C91 /* BaseHostingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseHostingViewController.swift; sourceTree = ""; }; 784AB0D82ABB15E9001801FC /* InputTextFieldView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputTextFieldView.swift; sourceTree = ""; }; 785AC5082AB2EBEE002C1BC1 /* AppDebugMode-iOS-Sample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "AppDebugMode-iOS-Sample.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 785AC50B2AB2EBEE002C1BC1 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -56,18 +50,12 @@ 785AC51B2AB2EBF0002C1BC1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 785AC5222AB2EDDE002C1BC1 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 785AC5242AB2EE0D002C1BC1 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = ""; }; - 785AC5272AB2EE66002C1BC1 /* DependencyContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DependencyContainer.swift; sourceTree = ""; }; + 785AC5272AB2EE66002C1BC1 /* SampleContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SampleContainer.swift; sourceTree = ""; }; 785AC52A2AB2EEA6002C1BC1 /* RequestManagerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManagerType.swift; sourceTree = ""; }; 785AC52C2AB2EED8002C1BC1 /* RequestManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestManager.swift; sourceTree = ""; }; - 785AC5312AB2EF81002C1BC1 /* FetchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchCoordinator.swift; sourceTree = ""; }; - 785AC5342AB2EFB0002C1BC1 /* FetchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchViewModel.swift; sourceTree = ""; }; - 785AC5362AB2F03F002C1BC1 /* FetchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchViewController.swift; sourceTree = ""; }; - 785AC53A2AB2F07E002C1BC1 /* BaseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseViewController.swift; sourceTree = ""; }; - 7874BEFB2AC1B39E007F0234 /* CacheManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManager.swift; sourceTree = ""; }; - 7874BEFD2AC1B3B5007F0234 /* CacheManagerType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheManagerType.swift; sourceTree = ""; }; - 7874BF002AC1B869007F0234 /* ProfileViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewModel.swift; sourceTree = ""; }; - 7874BF022AC1B902007F0234 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = ""; }; - 7874BF042AC1BB8E007F0234 /* ProfileCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileCoordinator.swift; sourceTree = ""; }; + 785AC5342AB2EFB0002C1BC1 /* ApiServerModeViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServerModeViewModel.swift; sourceTree = ""; }; + 785AC5362AB2F03F002C1BC1 /* ApiServerModeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServerModeView.swift; sourceTree = ""; }; + 7874BF022AC1B902007F0234 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = ""; }; 78A589F02AB88ADA004141F6 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; 78A589F32AB88BFC004141F6 /* CarResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarResponse.swift; sourceTree = ""; }; 78A589F52AB88D38004141F6 /* ProductResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductResponse.swift; sourceTree = ""; }; @@ -77,9 +65,7 @@ 78CDA6FF2ABAD2DF00CC1A82 /* HomeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeViewController.swift; sourceTree = ""; }; 78CDA7012ABAD47300CC1A82 /* HomeCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeCoordinator.swift; sourceTree = ""; }; 78CDA7032ABAD72300CC1A82 /* BasicButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BasicButton.swift; sourceTree = ""; }; - 78CDA7062ABAF3AD00CC1A82 /* LoginViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewModel.swift; sourceTree = ""; }; - 78CDA7082ABAF41500CC1A82 /* LoginViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginViewController.swift; sourceTree = ""; }; - 78CDA70A2ABAF68500CC1A82 /* LoginCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginCoordinator.swift; sourceTree = ""; }; + 78CDA7082ABAF41500CC1A82 /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = ""; }; 78E10E3C2AB9D9BA00934298 /* FetchButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchButton.swift; sourceTree = ""; }; 78E10E3E2AB9DDE000934298 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -147,9 +133,6 @@ 785AC5222AB2EDDE002C1BC1 /* Coordinator.swift */, 785AC5242AB2EE0D002C1BC1 /* AppCoordinator.swift */, 78CDA7012ABAD47300CC1A82 /* HomeCoordinator.swift */, - 78CDA70A2ABAF68500CC1A82 /* LoginCoordinator.swift */, - 785AC5312AB2EF81002C1BC1 /* FetchCoordinator.swift */, - 7874BF042AC1BB8E007F0234 /* ProfileCoordinator.swift */, ); path = Coordinators; sourceTree = ""; @@ -157,7 +140,7 @@ 785AC5262AB2EE48002C1BC1 /* DependencyContainer */ = { isa = PBXGroup; children = ( - 785AC5272AB2EE66002C1BC1 /* DependencyContainer.swift */, + 785AC5272AB2EE66002C1BC1 /* SampleContainer.swift */, ); path = DependencyContainer; sourceTree = ""; @@ -183,30 +166,22 @@ 785AC5332AB2EF99002C1BC1 /* Screens */ = { isa = PBXGroup; children = ( - 785AC5392AB2F06A002C1BC1 /* BaseViewController */, + 5D99DB172CBACBF9001B3C91 /* BaseHostingViewController.swift */, 78CDA6FC2ABAD25C00CC1A82 /* Home */, - 785AC5382AB2F062002C1BC1 /* Fetch */, - 78CDA7052ABAF39100CC1A82 /* Login */, - 7874BEFF2AC1B853007F0234 /* Profile */, + 785AC5382AB2F062002C1BC1 /* ApiServerMode */, + 78CDA7082ABAF41500CC1A82 /* LoginView.swift */, + 7874BF022AC1B902007F0234 /* ProfileView.swift */, ); path = Screens; sourceTree = ""; }; - 785AC5382AB2F062002C1BC1 /* Fetch */ = { + 785AC5382AB2F062002C1BC1 /* ApiServerMode */ = { isa = PBXGroup; children = ( - 785AC5342AB2EFB0002C1BC1 /* FetchViewModel.swift */, - 785AC5362AB2F03F002C1BC1 /* FetchViewController.swift */, + 785AC5342AB2EFB0002C1BC1 /* ApiServerModeViewModel.swift */, + 785AC5362AB2F03F002C1BC1 /* ApiServerModeView.swift */, ); - path = Fetch; - sourceTree = ""; - }; - 785AC5392AB2F06A002C1BC1 /* BaseViewController */ = { - isa = PBXGroup; - children = ( - 785AC53A2AB2F07E002C1BC1 /* BaseViewController.swift */, - ); - path = BaseViewController; + path = ApiServerMode; sourceTree = ""; }; 785AC53C2AB2F3D4002C1BC1 /* Application */ = { @@ -223,30 +198,11 @@ isa = PBXGroup; children = ( 785AC5262AB2EE48002C1BC1 /* DependencyContainer */, - 7874BEFA2AC1B37A007F0234 /* CacheManager */, 785AC5292AB2EE93002C1BC1 /* RequestManager */, ); path = Managers; sourceTree = ""; }; - 7874BEFA2AC1B37A007F0234 /* CacheManager */ = { - isa = PBXGroup; - children = ( - 7874BEFB2AC1B39E007F0234 /* CacheManager.swift */, - 7874BEFD2AC1B3B5007F0234 /* CacheManagerType.swift */, - ); - path = CacheManager; - sourceTree = ""; - }; - 7874BEFF2AC1B853007F0234 /* Profile */ = { - isa = PBXGroup; - children = ( - 7874BF002AC1B869007F0234 /* ProfileViewModel.swift */, - 7874BF022AC1B902007F0234 /* ProfileViewController.swift */, - ); - path = Profile; - sourceTree = ""; - }; 78A589F22AB88BE3004141F6 /* Models */ = { isa = PBXGroup; children = ( @@ -290,15 +246,6 @@ path = Home; sourceTree = ""; }; - 78CDA7052ABAF39100CC1A82 /* Login */ = { - isa = PBXGroup; - children = ( - 78CDA7062ABAF3AD00CC1A82 /* LoginViewModel.swift */, - 78CDA7082ABAF41500CC1A82 /* LoginViewController.swift */, - ); - path = " Login"; - sourceTree = ""; - }; 78E10E3A2AB9D98600934298 /* Views */ = { isa = PBXGroup; children = ( @@ -399,33 +346,26 @@ 78CDA6FE2ABAD27B00CC1A82 /* HomeViewModel.swift in Sources */, 785AC5252AB2EE0D002C1BC1 /* AppCoordinator.swift in Sources */, 78E10E3F2AB9DDE000934298 /* Constants.swift in Sources */, + 5D99DB182CBACC02001B3C91 /* BaseHostingViewController.swift in Sources */, 78A589F42AB88BFC004141F6 /* CarResponse.swift in Sources */, 78E10E3D2AB9D9BA00934298 /* FetchButton.swift in Sources */, 78CDA7002ABAD2DF00CC1A82 /* HomeViewController.swift in Sources */, - 7874BEFE2AC1B3B5007F0234 /* CacheManagerType.swift in Sources */, 78CDA7042ABAD72300CC1A82 /* BasicButton.swift in Sources */, - 785AC5352AB2EFB0002C1BC1 /* FetchViewModel.swift in Sources */, - 785AC53B2AB2F07E002C1BC1 /* BaseViewController.swift in Sources */, + 785AC5352AB2EFB0002C1BC1 /* ApiServerModeViewModel.swift in Sources */, 785AC5232AB2EDDE002C1BC1 /* Coordinator.swift in Sources */, - 78CDA7092ABAF41500CC1A82 /* LoginViewController.swift in Sources */, - 7874BEFC2AC1B39E007F0234 /* CacheManager.swift in Sources */, - 785AC5282AB2EE66002C1BC1 /* DependencyContainer.swift in Sources */, + 78CDA7092ABAF41500CC1A82 /* LoginView.swift in Sources */, + 785AC5282AB2EE66002C1BC1 /* SampleContainer.swift in Sources */, 78A589F62AB88D38004141F6 /* ProductResponse.swift in Sources */, - 7874BF052AC1BB8E007F0234 /* ProfileCoordinator.swift in Sources */, 785AC52D2AB2EED8002C1BC1 /* RequestManager.swift in Sources */, 5D8F1E662B8F2EDD00D84DBE /* LargeObjectResponse.swift in Sources */, - 785AC5372AB2F03F002C1BC1 /* FetchViewController.swift in Sources */, + 785AC5372AB2F03F002C1BC1 /* ApiServerModeView.swift in Sources */, 785AC52B2AB2EEA6002C1BC1 /* RequestManagerType.swift in Sources */, - 785AC5322AB2EF81002C1BC1 /* FetchCoordinator.swift in Sources */, - 78CDA70B2ABAF68500CC1A82 /* LoginCoordinator.swift in Sources */, 78A589F92AB89421004141F6 /* UIControlExtensions.swift in Sources */, 78A589F12AB88ADA004141F6 /* Endpoint.swift in Sources */, 785AC50C2AB2EBEE002C1BC1 /* AppDelegate.swift in Sources */, - 7874BF032AC1B902007F0234 /* ProfileViewController.swift in Sources */, + 7874BF032AC1B902007F0234 /* ProfileView.swift in Sources */, 78CDA7022ABAD47300CC1A82 /* HomeCoordinator.swift in Sources */, - 78CDA7072ABAF3AD00CC1A82 /* LoginViewModel.swift in Sources */, 784AB0D92ABB15E9001801FC /* InputTextFieldView.swift in Sources */, - 7874BF012AC1B869007F0234 /* ProfileViewModel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -494,7 +434,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_NSMainNibFile = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; @@ -549,7 +489,7 @@ GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; INFOPLIST_KEY_NSMainNibFile = ""; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; @@ -575,7 +515,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportsDocumentBrowser = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -584,7 +524,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.goodrequest.appDebugModeiOS.sample.AppDebugMode-iOS-Sample"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -606,7 +546,7 @@ INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportsDocumentBrowser = YES; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -615,7 +555,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.goodrequest.appDebugModeiOS.sample.AppDebugMode-iOS-Sample"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 0465579..379d927 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,70 +1,105 @@ { - "object": { - "pins": [ - { - "package": "Alamofire", - "repositoryURL": "https://github.com/Alamofire/Alamofire.git", - "state": { - "branch": null, - "revision": "723fa5a6c65812aec4a0d7cc432ee198883b6e00", - "version": "5.9.0" - } - }, - { - "package": "AlamofireImage", - "repositoryURL": "https://github.com/Alamofire/AlamofireImage.git", - "state": { - "branch": null, - "revision": "1eaf3b6c6882bed10f6e7b119665599dd2329aa1", - "version": "4.3.0" - } - }, - { - "package": "CombineExt", - "repositoryURL": "https://github.com/CombineCommunity/CombineExt.git", - "state": { - "branch": null, - "revision": "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", - "version": "1.8.1" - } - }, - { - "package": "GoodNetworking", - "repositoryURL": "https://github.com/GoodRequest/GoodNetworking.git", - "state": { - "branch": null, - "revision": "47b616af6b839166cdac357d02cbce6b65c959c0", - "version": "2.5.0" - } - }, - { - "package": "GoodPersistence", - "repositoryURL": "https://github.com/GoodRequest/GoodPersistence.git", - "state": { - "branch": null, - "revision": "841f777b44a6061e3a6f4e01ffb1e36af2dfe13f", - "version": "2.1.0" - } - }, - { - "package": "KeychainAccess", - "repositoryURL": "https://github.com/kishikawakatsumi/KeychainAccess.git", - "state": { - "branch": null, - "revision": "84e546727d66f1adc5439debad16270d0fdd04e7", - "version": "4.2.2" - } - }, - { - "package": "swiftui-introspect", - "repositoryURL": "https://github.com/siteline/swiftui-introspect", - "state": { - "branch": null, - "revision": "0cd2a5a5895306bc21d54a2254302d24a9a571e4", - "version": "1.1.3" - } + "originHash" : "11e672e30d756c2fc3f7ae187f2eeb3995eb992a570a647e92d895d6d9451cf0", + "pins" : [ + { + "identity" : "alamofire", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/Alamofire.git", + "state" : { + "revision" : "ea6a94b7dddffd0ca4d0f29252d95310b84dec84", + "version" : "5.10.0" } - ] - }, - "version": 1 + }, + { + "identity" : "alamofireimage", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Alamofire/AlamofireImage.git", + "state" : { + "revision" : "1eaf3b6c6882bed10f6e7b119665599dd2329aa1", + "version" : "4.3.0" + } + }, + { + "identity" : "chronometer", + "kind" : "remoteSourceControl", + "location" : "https://github.com/KittyMac/Chronometer.git", + "state" : { + "revision" : "d21b89e5cb5929b5175bdb3ad710a52cbf497eaa", + "version" : "0.1.12" + } + }, + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory.git", + "state" : { + "revision" : "587995f7d5cc667951d635fbf6b4252324ba0439", + "version" : "2.3.2" + } + }, + { + "identity" : "goodlogger", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GoodRequest/GoodLogger.git", + "state" : { + "revision" : "3c1ef9289d23154fab9124f58c356a61917646d2", + "version" : "1.2.3" + } + }, + { + "identity" : "goodnetworking", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GoodRequest/GoodNetworking.git", + "state" : { + "revision" : "a9c89c3713bf88b425935f2ccfbb807b24d2d5ac", + "version" : "3.2.0" + } + }, + { + "identity" : "hitch", + "kind" : "remoteSourceControl", + "location" : "https://github.com/KittyMac/Hitch.git", + "state" : { + "revision" : "d6c147a1d70992db39a141cb5bf9cf8fbb776250", + "version" : "0.4.148" + } + }, + { + "identity" : "pulse", + "kind" : "remoteSourceControl", + "location" : "https://github.com/kean/Pulse.git", + "state" : { + "revision" : "d6793206824303ccf19228bdce77d0f270867cd4", + "version" : "5.1.1" + } + }, + { + "identity" : "sextant", + "kind" : "remoteSourceControl", + "location" : "https://github.com/KittyMac/Sextant.git", + "state" : { + "revision" : "52a77d0bce0210cf9557faef7fd0adb9a6da02fb", + "version" : "0.4.31" + } + }, + { + "identity" : "spanker", + "kind" : "remoteSourceControl", + "location" : "https://github.com/KittyMac/Spanker.git", + "state" : { + "revision" : "d4b439bf76a40fb45d86a24d13b9c26e7d630eee", + "version" : "0.2.49" + } + }, + { + "identity" : "swiftui-introspect", + "kind" : "remoteSourceControl", + "location" : "https://github.com/siteline/swiftui-introspect", + "state" : { + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" + } + } + ], + "version" : 3 } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Application/AppDelegate.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Application/AppDelegate.swift index 9a2a0bd..ea4eaf9 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Application/AppDelegate.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Application/AppDelegate.swift @@ -9,12 +9,13 @@ import SwiftUI #if DEBUG import AppDebugMode #endif +import Factory @main +@MainActor class AppDelegate: UIResponder, UIApplicationDelegate { var window: UIWindow? - private var dependencyContainer = DependencyContainer() #if DEBUG var model: CustomControlsModel = CustomControlsModel() @@ -28,15 +29,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate { window = UIWindow() #if DEBUG - AppDebugModeProvider.shared.setup( - serversCollections: Constants.ServersCollections.allClases, - onServerChange: { debugPrint("Server has been changed") }, - cacheManager: dependencyContainer.cacheManager, - customControls: { CustomControlsView(model: model) } - ) + Task { + let providers = [ + DebugSelectableServerProvider( + apiServerPickerConfiguration: .init( + serversCollections: Constants.devServerCollection, + onSelectedServerChange: nil + ) + ), + DebugSelectableServerProvider( + apiServerPickerConfiguration: .init( + serversCollections: Constants.testServerCollection, + onSelectedServerChange: nil + ) + ) + ] + + await PackageManager.shared.setup( + serverProviders: providers, + configurableProxySessionProvider: Container.shared.configurableSessionProvider.resolve(), + customControls: CustomControlsView(model: model), + showDebugSwift: true + ) + } + #endif - - AppCoordinator(window: window, di: dependencyContainer).start() + + Task { + await AppCoordinator(window: window).start() + } return true } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/AppCoordinator.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/AppCoordinator.swift index 4e881bb..d73dd25 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/AppCoordinator.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/AppCoordinator.swift @@ -7,25 +7,24 @@ import UIKit -enum AppStep { +enum AppStep: Sendable { case home(HomeStep) } +@MainActor final class AppCoordinator: Coordinator { private let window: UIWindow? - private let di: DependencyContainer - init(window: UIWindow?, di: DependencyContainer) { + init(window: UIWindow?) { self.window = window - self.di = di } @discardableResult - override func start() -> UIViewController? { - window?.rootViewController = HomeCoordinator(di: di).start() + override func start() async -> UIViewController? { + window?.rootViewController = await HomeCoordinator().start() window?.makeKeyAndVisible() return nil diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/Coordinator.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/Coordinator.swift index 8a9d0f1..b7f62d0 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/Coordinator.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/Coordinator.swift @@ -7,7 +7,8 @@ import UIKit -class Coordinator { +@MainActor +class Coordinator { var rootViewController: UIViewController? @@ -19,7 +20,7 @@ class Coordinator { return rootViewController as? UINavigationController } - func start() -> UIViewController? { + func start() async -> UIViewController? { return rootViewController } @@ -27,6 +28,6 @@ class Coordinator { self.rootViewController = rootViewController } - func navigate(to stepper: Step) {} + func navigate(to stepper: Step) async {} } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/FetchCoordinator.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/FetchCoordinator.swift deleted file mode 100644 index 79114e7..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/FetchCoordinator.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// HomeCoordinator.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 14/09/2023. -// - -import UIKit - -enum FetchStep { } - -final class FetchCoordinator: Coordinator { - - private let di: DependencyContainer - - init(di: DependencyContainer, rootViewController: UINavigationController?) { - self.di = di - super.init(rootViewController: rootViewController) - } - - override func start() -> UIViewController? { - let fetchViewModel = FetchViewModel(di: di, coordinator: self) - let fetchViewController = FetchViewController(viewModel: fetchViewModel) - - return fetchViewController - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/HomeCoordinator.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/HomeCoordinator.swift index 3857500..825639b 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/HomeCoordinator.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/HomeCoordinator.swift @@ -9,22 +9,20 @@ import UIKit enum HomeStep { - case goToFetch - case goToLogin - case goToSettings + case goToAPIServerMode + case goToUserLoginMode + case goToUserProfile } +@MainActor final class HomeCoordinator: Coordinator { - private let di: DependencyContainer - - init(di: DependencyContainer) { - self.di = di + init() { super.init(rootViewController: UINavigationController()) } - override func start() -> UIViewController? { + override func start() async -> UIViewController? { let homeViewModel = HomeViewModel(coordinator: self) let homeViewController = HomeViewController(viewModel: homeViewModel) @@ -33,47 +31,24 @@ final class HomeCoordinator: Coordinator { return rootViewController } - override func navigate(to stepper: AppStep) { + override func navigate(to stepper: AppStep) async { switch stepper { case .home(let homeStep): - navigate(to: homeStep) + await navigate(to: homeStep) } } - func navigate(to step: HomeStep) { + func navigate(to step: HomeStep) async { switch step { - case .goToFetch: - guard let fetchViewController = FetchCoordinator( - di: di, - rootViewController: rootNavigationController - ).start() - else { - return - } - - navigationController?.pushViewController(fetchViewController, animated: true) - - case .goToLogin: - guard let loginViewController = LoginCoordinator( - di: di, - rootViewController: rootNavigationController - ).start() - else { - return - } - - navigationController?.pushViewController(loginViewController, animated: true) - - case .goToSettings: - guard let settingsViewController = ProfileCoordinator( - di: di, - rootViewController: rootNavigationController - ).start() - else { - return - } - - navigationController?.pushViewController(settingsViewController, animated: true) + case .goToAPIServerMode: + let controller = ApiServerModeView(viewModel: .init()).eraseToUIViewController() + navigationController?.pushViewController(controller, animated: true) + + case .goToUserLoginMode: + navigationController?.pushViewController(LoginView().eraseToUIViewController(), animated: true) + + case .goToUserProfile: + navigationController?.pushViewController(ProfileView().eraseToUIViewController(), animated: true) } } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/LoginCoordinator.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/LoginCoordinator.swift deleted file mode 100644 index e1f0475..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/LoginCoordinator.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// LoginCoordinator.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 20/09/2023. -// - -import UIKit - -enum LoginStep { } - -final class LoginCoordinator: Coordinator { - - private let di: DependencyContainer - - init(di: DependencyContainer, rootViewController: UINavigationController?) { - self.di = di - super.init(rootViewController: rootViewController) - } - - override func start() -> UIViewController? { - let fetchViewModel = LoginViewModel(coordinator: self) - let fetchViewController = LoginViewController(viewModel: fetchViewModel) - - return fetchViewController - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/ProfileCoordinator.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/ProfileCoordinator.swift deleted file mode 100644 index 17c01d7..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Coordinators/ProfileCoordinator.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// SettingsCoordinator.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 25/09/2023. -// - -import UIKit - -enum ProfileStep { } - -final class ProfileCoordinator: Coordinator { - - private let di: DependencyContainer - - init(di: DependencyContainer, rootViewController: UINavigationController?) { - self.di = di - super.init(rootViewController: rootViewController) - } - - override func start() -> UIViewController? { - let fetchViewModel = ProfileViewModel(di: di, coordinator: self) - let fetchViewController = ProfileViewController(viewModel: fetchViewModel) - - return fetchViewController - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Extensions/UIControlExtensions.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Extensions/UIControlExtensions.swift index d9239d3..c794e3b 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Extensions/UIControlExtensions.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Extensions/UIControlExtensions.swift @@ -10,6 +10,7 @@ import Combine extension UIControl { + @MainActor class InteractionSubscription: Subscription where S.Input == Void { private let subscriber: S? @@ -28,12 +29,12 @@ extension UIControl { _ = self.subscriber?.receive(()) } - func request(_ demand: Subscribers.Demand) {} + nonisolated func request(_ demand: Subscribers.Demand) {} - func cancel() {} + nonisolated func cancel() {} } - struct InteractionPublisher: Publisher { + struct InteractionPublisher: @preconcurrency Publisher { typealias Output = Void typealias Failure = Never @@ -46,6 +47,7 @@ extension UIControl { self.event = event } + @MainActor func receive(subscriber: S) where S : Subscriber, Never == S.Failure, Void == S.Input { let subscription = InteractionSubscription( subscriber: subscriber, diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Helpers/Constants.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Helpers/Constants.swift index 6010f6a..19836f2 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Helpers/Constants.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Helpers/Constants.swift @@ -5,102 +5,36 @@ // Created by Lukas Kubaliak on 19/09/2023. // -import Foundation +import GoodNetworking #if DEBUG import AppDebugMode #endif + struct Constants { - // MARK: Insets - - struct Insets { - - static let edgeInset = 16.0 - - } - - // MARK: - Texts - - struct Texts { - - struct Home { - - static let title = "App Debug Mode" - static let description = "...allows an iOS application to select an API Server and User for the app during runtime" - static let login = "User Login Mode" - static let fetch = "API Server Mode" - static let userDefaults = "User Profile Mode" - - } - - struct Fetch { - - static let fetchLarge = "Fetch Large" - static let fetch = "Tap to fetch" - static let placeHolder = "Empty" - - } - - struct Login { - - static let loginTitle = "Name" - static let loginPlaceholder = "Enter your login name" - static let passwordTitle = "Password" - static let passwordPlaceholder = "Enter your login password" - - } - - struct Profile { - - static let nameTitle = "Full name" - static let namePlaceholder = "Enter your full name" - static let genderOptions = ["Male", "Female"] - - } - - } - // MARK: - Servers - - enum ProdServer { - - static let name = "CARS" - static let url = "https://myfakeapi.com/api/" - - } - - enum DevServer { - - static let name = "PRODUCTS" - static let url = "https://fakestoreapi.com/" - - } - + #if DEBUG - enum ServersCollections { - - static let sampleBackend = ApiServerCollection( - name: "SAMPLE backend", - servers: [ - Constants.Servers.prod, - Constants.Servers.dev - ], - defaultSelectedServer: Constants.Servers.dev - ) - - static var allClases: [ApiServerCollection] = [ - Self.sampleBackend - ] - - } - - enum Servers { + static let devServer = AppDebugMode.ApiServer(name: "PRODUCTS", url: "https://fakestoreapi.com/") + static let prodServer = AppDebugMode.ApiServer(name: "CARS", url: "https://myfakeapi.com/api/") + static let testServer = AppDebugMode.ApiServer(name: "TEST", url: "https://myfakeapi.com/api/test") + + static let devServerCollection = AppDebugMode.ApiServerCollection( + name: "Development Servers", + servers: [devServer, prodServer], + defaultServer: devServer + ) + + static let testServerCollection = AppDebugMode.ApiServerCollection( + name: "Test Servers", + servers: [devServer, prodServer], + defaultServer: devServer + ) + #else + + static let prodServer = "https://myfakeapi.com/api/" - static let prod = ApiServer(name: ProdServer.name, url: ProdServer.url) - static let dev = ApiServer(name: DevServer.name, url: DevServer.url) - - } #endif } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManager.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManager.swift deleted file mode 100644 index 9292aa9..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManager.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// CacheManager.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 25/09/2023. -// - -import GoodPersistence - -final class CacheManager: CacheManagerType { - - @UserDefaultValue("favoritePet", defaultValue: "Penguin") - var favoritePet: String - - @UserDefaultValue("fullName", defaultValue: nil) - var fullName: String? - - @KeychainValue("gender", defaultValue: 0) - var gender: Int - - @UserDefaultValue("favoriteColor", defaultValue: "Red") - var favoriteColor: String - - // MARK: - Update - - func updateFullName(with name: String) { - self.fullName = name - } - - func udpateGender(with gender: Int) { - self.gender = gender - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManagerType.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManagerType.swift deleted file mode 100644 index 1228256..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/CacheManager/CacheManagerType.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// CacheManagerType.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 25/09/2023. -// - -import Foundation - -protocol CacheManagerType { - - var fullName: String? { get set } - var gender: Int { get set } - - // MARK: - Update - - func updateFullName(with name: String) - func udpateGender(with gender: Int) - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/DependencyContainer.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/DependencyContainer.swift deleted file mode 100644 index dd5e1e8..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/DependencyContainer.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// DependencyContainer.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 14/09/2023. -// - -import Foundation - -protocol WithRequestManager: AnyObject { - - var requestManager: RequestManagerType { get } - -} - -protocol WithCacheManager: AnyObject { - - var cacheManager: CacheManagerType { get } - -} - -final class DependencyContainer: WithRequestManager, WithCacheManager { - - lazy var requestManager: RequestManagerType = RequestManager(baseServer: .base) - lazy var cacheManager: CacheManagerType = CacheManager() - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/SampleContainer.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/SampleContainer.swift new file mode 100644 index 0000000..ed29d03 --- /dev/null +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/DependencyContainer/SampleContainer.swift @@ -0,0 +1,56 @@ +// +// Container.swift +// AppDebugMode-iOS-Sample +// +// Created by Andrej Jaššo on 20/09/2024. +// + +import Factory +import GoodNetworking + +#if DEBUG +import AppDebugMode +import Alamofire +import GoodLogger +#endif + +extension Container { + + var requestManager: Factory { + Factory(self) { + RequestManager(baseUrlProvider: self.urlProvider.resolve()) + }.singleton + } + + var urlProvider: Factory { + Factory(self) { + self.serverSelector.resolve() + }.singleton + } + + #if DEBUG + var serverSelector: Factory { + Factory(self) { + DebugSelectableServerProvider(apiServerPickerConfiguration: .init(serversCollections: Constants.devServerCollection)) + }.singleton + } + #else + var serverSelector: Factory { + Factory(self) { Constants.prodServer }.singleton + } + #endif + + #if DEBUG + var configurableSessionProvider: Factory { + LoggingEventMonitor.maxVerboseLogSizeBytes = 1000000 + let monitor = LoggingEventMonitor(logger: AppDebugModeLogger()) + + let defaultConfiguration = NetworkSessionConfiguration( + urlSessionConfiguration: .default, + eventMonitors: [monitor] + ) + return Factory(self) { ConfigurableSessionProvider(defaultConfiguration: defaultConfiguration) }.singleton + } + #endif + +} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManager.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManager.swift index d3257bc..0938f61 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManager.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManager.swift @@ -6,70 +6,55 @@ // import Foundation -import Combine import GoodNetworking -import Alamofire +@preconcurrency import Alamofire +import GoodLogger + #if DEBUG import AppDebugMode +import Factory #endif -final class RequestManager: RequestManagerType { - - enum ApiServer: String { - - case base - - var rawValue: String { - #if DEBUG - return AppDebugModeProvider.shared.getSelectedServer(for: Constants.ServersCollections.sampleBackend).url - #else - return Constants.ProdServer.url - #endif - } - - } - - private let session: NetworkSession +actor RequestManager: RequestManagerType { + +#if DEBUG + @Injected(\.configurableSessionProvider) private var sessionProvider: ConfigurableSessionProvider +#endif + + private var session: NetworkSession - init(baseServer: ApiServer) { - LoggingEventMonitor.maxVerboseLogSizeBytes = 1000000 - let monitor = LoggingEventMonitor(logger: nil) - #if DEBUG - StandardOutputService.shared.connectCustomLogStreamPublisher(monitor.subscribeToMessages()) - #endif + init(baseUrlProvider: BaseUrlProviding) { - #if DEBUG - let urlSessionConfig = AppDebugModeProvider.shared.proxySettingsProvider.urlSessionConfiguration - #else - let urlSessionConfig = URLSessionConfiguration.default - #endif +#if DEBUG + session = NetworkSession( + baseUrl: baseUrlProvider, + configuration: Container.shared.configurableSessionProvider.resolve() + ) +#else + let monitor = LoggingEventMonitor(logger: OSLogLogger()) session = NetworkSession( - baseUrl: baseServer.rawValue, - configuration: NetworkSessionConfiguration( - urlSessionConfiguration: urlSessionConfig, - interceptor: nil, - eventMonitors: [monitor] + baseUrl: baseUrlProvider, + configuration: DefaultSessionProvider(configuration: + NetworkSessionConfiguration( + urlSessionConfiguration: .default, + eventMonitors: [monitor] + ) ) ) +#endif } - func fetchLarge() -> AnyPublisher { - session.request(endpoint: Endpoint.large, base: "https://codepo8.github.io") - .goodify() - .eraseToAnyPublisher() + func fetchLargeObject() async throws -> LargeObjectResponse { + try await session.request(endpoint: Endpoint.large, baseUrl: "https://codepo8.github.io") } - func fetchCars(id: Int) -> AnyPublisher { - return session.request(endpoint: Endpoint.cars(id)) - .goodify() - .eraseToAnyPublisher() + func fetchCars(id: Int) async throws -> CarResponse { + try await session.request(endpoint: Endpoint.cars(id)) } - - func fetchProducts(id: Int) -> AnyPublisher { - return session.request(endpoint: Endpoint.products(id)) - .goodify() - .eraseToAnyPublisher() + + func fetchProducts(id: Int) async throws -> ProductResponse { + try await session.request(endpoint: Endpoint.products(id)) } - + } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManagerType.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManagerType.swift index f657965..42ff41b 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManagerType.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Managers/RequestManager/RequestManagerType.swift @@ -9,10 +9,10 @@ import Foundation import Alamofire import Combine -protocol RequestManagerType: AnyObject { +protocol RequestManagerType: Sendable { - func fetchCars(id: Int) -> AnyPublisher - func fetchProducts(id: Int) -> AnyPublisher - func fetchLarge() -> AnyPublisher + func fetchCars(id: Int) async throws -> CarResponse + func fetchProducts(id: Int) async throws -> ProductResponse + func fetchLargeObject() async throws -> LargeObjectResponse } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewController.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewController.swift deleted file mode 100644 index 910649f..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewController.swift +++ /dev/null @@ -1,100 +0,0 @@ -// -// LoginViewController.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 20/09/2023. -// - -import UIKit -import Combine -#if DEBUG -import AppDebugMode -#endif - -final class LoginViewController: BaseViewController { - - // MARK: - Constants - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.spacing = 16 - - return stackView - }() - - private let loginNameTextView: InputTextFieldView = { - let textView = InputTextFieldView() - textView.configure( - with: InputTextFieldView.Model( - title: Constants.Texts.Login.loginTitle, - placeholder: Constants.Texts.Login.loginPlaceholder - ) - ) - - return textView - }() - - private let passwordNameTextView: InputTextFieldView = { - let textView = InputTextFieldView() - textView.configure( - with: InputTextFieldView.Model( - title: Constants.Texts.Login.passwordTitle, - placeholder: Constants.Texts.Login.passwordPlaceholder - ) - ) - - return textView - }() - -} - -// MARK: - Lifecycle - -extension LoginViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - setupLayout() - - #if DEBUG - AppDebugModeProvider.shared.selectedTestingUserPublisher - .sink { [weak self] in - self?.loginNameTextView.set(text: $0?.name) - self?.passwordNameTextView.set(text: $0?.password) - } - .store(in: &cancellables) - #endif - } - -} - -// MARK: - Setup - -private extension LoginViewController { - - func setupLayout() { - view.backgroundColor = .white - navigationController?.navigationBar.prefersLargeTitles = true - title = Constants.Texts.Home.login - - addSubviews() - setupConstraints() - } - - func addSubviews() { - view.addSubview(stackView) - [loginNameTextView, passwordNameTextView].forEach { stackView.addArrangedSubview($0) } - } - - func setupConstraints() { - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Insets.edgeInset * 2), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Insets.edgeInset * 2) - ]) - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewModel.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewModel.swift deleted file mode 100644 index 728df7d..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ Login/LoginViewModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// LoginViewModel.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 20/09/2023. -// - -import Combine -#if DEBUG -import AppDebugMode -#endif - -final class LoginViewModel { - - // MARK: - Constants - - private let coordinator: Coordinator - - // MARK: - Initializer - - init(coordinator: Coordinator) { - self.coordinator = coordinator - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeView.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeView.swift new file mode 100644 index 0000000..5e44320 --- /dev/null +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeView.swift @@ -0,0 +1,132 @@ +// +// APIServerModeView.swift +// AppDebugMode-iOS-Sample +// +// Created by Lukas Kubaliak on 14/09/2023. +// + +import SwiftUI + +struct ApiServerModeView: View { + + // MARK: - State + + @State private var selectedServerName: String = "" + @State private var selectedServerURL: String = "" + @State private var simpleResponse: String = "No response processed" + @State private var largeResponse: String = "No response processed" + @State private var isFetchingSimpleResponse: Bool = false + @State private var isFetchingLargeObject: Bool = false + + + init(viewModel: ApiServerModeViewModel) { + self.viewModel = viewModel + } + + @ObservedObject var viewModel: ApiServerModeViewModel + + // MARK: - Body + var body: some View { + NavigationView { + GeometryReader { proxy in + VStack { + HStack { + Button(action: fetchSimpleResponse) { + Text(isFetchingSimpleResponse ? "Fetching..." : "Fetch Simple Response") + } + .buttonStyle(FetchButtonStyle(isLoading: isFetchingSimpleResponse)) + + Button(action: fetchLargeObject) { + Text(isFetchingLargeObject ? "Fetching..." : "Fetch Large Object") + } + .buttonStyle(FetchButtonStyle(isLoading: isFetchingLargeObject)) + } + + Group { + Text("\(viewModel.selectedServerName): \(viewModel.selectedServerURL)") + + Text(simpleResponse) + + Text(largeResponse) + + } + .frame(width: proxy.size.width) + .lineLimit(2) + .foregroundColor(.secondary) + .padding() + } + } + } + .navigationTitle("API Server Mode") + .onAppear { + self.selectedServerName = viewModel.selectedServerName + self.selectedServerURL = viewModel.selectedServerURL + } + } + + // MARK: - Action Methods + + private func fetchSimpleResponse() { + isFetchingSimpleResponse = true + Task { + await viewModel.fetchSimpleResponse() + handleSimpleResponse() + isFetchingSimpleResponse = false + } + } + + private func fetchLargeObject() { + isFetchingLargeObject = true + Task { + await viewModel.fetchLargeObject() + handleLargeResponse() + isFetchingLargeObject = false + } + } + + // MARK: - Handle Responses + + private func handleSimpleResponse() { + switch viewModel.carResult { + case .idle: + simpleResponse = "No response processed" + case .loading: + simpleResponse = "Loading..." + case .success(let carResponse): + if let data = try? JSONEncoder().encode(carResponse) { + simpleResponse = String(data: data, encoding: .utf8) ?? "Invalid response" + } + case .error(let error): + simpleResponse = error.localizedDescription + } + } + + private func handleLargeResponse() { + switch viewModel.largeResult { + case .idle: + largeResponse = "No response processed" + case .loading: + largeResponse = "Loading..." + case .success(let largeResponseData): + if let data = try? JSONEncoder().encode(largeResponseData) { + largeResponse = String(data: data, encoding: .utf8)?.prefix(5_000).appending("...") ?? "Invalid response" + } + case .error(let error): + largeResponse = error.localizedDescription + } + } +} + +struct FetchButtonStyle: ButtonStyle { + var isLoading: Bool + + func makeBody(configuration: Configuration) -> some View { + configuration.label + .padding() + .background(isLoading ? Color.gray : Color.blue) + .foregroundColor(.white) + .cornerRadius(8) + .opacity(configuration.isPressed ? 0.7 : 1.0) + .animation(.easeInOut, value: isLoading) + } +} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeViewModel.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeViewModel.swift new file mode 100644 index 0000000..3753424 --- /dev/null +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ApiServerMode/ApiServerModeViewModel.swift @@ -0,0 +1,115 @@ +// +// APIServerModeViewModel.swift +// AppDebugMode-iOS-Sample +// +// Created by Lukas Kubaliak on 14/09/2023. +// + +import Combine +import Alamofire +#if DEBUG +import AppDebugMode +#endif +import Factory + +@MainActor +final class ApiServerModeViewModel: ObservableObject { + + // MARK: - Factory + + @Injected(\.requestManager) private var requestManager + @Injected(\.urlProvider) private var urlProvider + + // MARK: - Enums + + enum CarFetchingState { + + case idle + case loading + case success(CarResponse) + case error(AFError) + + } + + enum LargeFetchingState { + + case idle + case loading + case success(LargeObjectResponse) + case error(AFError) + + } + + enum ProductFetchingState { + + case idle + case loading + case success(ProductResponse) + case error(AFError) + + } + + // MARK: - Variables - Computed + + var selectedServerName: String = "" + var selectedServerURL: String = "" + + // MARK: - Combine + + @Published var largeResult: LargeFetchingState = .idle + @Published var carResult: CarFetchingState = .idle + @Published var productResult: ProductFetchingState = .idle + + // MARK: - Initializer + + init() { + Task { + self.selectedServerName = await Container.shared.urlProvider.resolve().resolveBaseUrl() ?? "" + } + } + +} + +extension ApiServerModeViewModel { + + func fetchSimpleResponse() async { + #if DEBUG + await selectedServerName == Constants.devServer.name ? fetchProduct() : fetchCar() + #else + await selectedServerName == Constants.prodServer ? fetchProduct() : fetchCar() + #endif + } + + func fetchLargeObject() async { + do { + largeResult = .success(try await requestManager.fetchLargeObject()) + } catch { + if let error = error as? AFError { + largeResult = .error(error) + } + } + } + + // MARK: - Helpers + + private func fetchCar() async { + do { + carResult = .success(try await requestManager.fetchCars(id: Int.random(in: 1...10))) + } catch { + if let error = error as? AFError { + carResult = .error(error) + } + } + } + + private func fetchProduct() async { + do { + productResult = .success(try await requestManager.fetchProducts(id: Int.random(in: 1...10))) + } catch { + if let error = error as? AFError { + productResult = .error(error) + } + } + } + +} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseHostingViewController.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseHostingViewController.swift new file mode 100644 index 0000000..ec04095 --- /dev/null +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseHostingViewController.swift @@ -0,0 +1,49 @@ +// +// BaseHostingViewController.swift +// AppDebugMode-iOS-Sample +// +// Created by Andrej Jasso on 12/10/2024. +// + +import SwiftUI +import Combine + +extension View { + + func eraseToUIViewController() -> BaseHostingController { + BaseHostingController(contentView: self) + } + +} + +final class BaseHostingController: UIHostingController { + + init( + contentView: Content + ) { + let view = contentView + + super.init(rootView: AnyView(view)) + } + + @MainActor @objc required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + } + +} + +extension BaseHostingController: UIPopoverPresentationControllerDelegate { + + func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { + return .none + } + +} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseViewController/BaseViewController.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseViewController/BaseViewController.swift deleted file mode 100644 index dca3bf1..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/BaseViewController/BaseViewController.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// BaseViewController.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 14/09/2023. -// - -import UIKit -import Combine - -class BaseViewController: UIViewController { - - let viewModel: T - var cancellables = Set() - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - required init(viewModel: T) { - self.viewModel = viewModel - - super.init(nibName: nil, bundle: nil) - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewController.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewController.swift deleted file mode 100644 index ed36dc6..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewController.swift +++ /dev/null @@ -1,193 +0,0 @@ -// -// HomeViewController.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 14/09/2023. -// - -import UIKit -import Combine - -final class FetchViewController: BaseViewController { - - // MARK: - Constants - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.spacing = 32 - - return stackView - }() - - private let serverLabel: UILabel = { - let label = UILabel() - label.textAlignment = .center - - return label - }() - - private let responseLabel: UILabel = { - let label = UILabel() - label.textColor = .secondaryLabel - label.numberOfLines = 0 - label.textAlignment = .center - - return label - }() - - private let fetchButton: FetchButton = { - let button = FetchButton() - button.setTitle(Constants.Texts.Fetch.fetch, for: .normal) - - return button - }() - - private let fetchLarge: FetchButton = { - let button = FetchButton() - button.setTitle(Constants.Texts.Fetch.fetchLarge, for: .normal) - - return button - }() - -} - -// MARK: - Lifecycle - -extension FetchViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - setupLayout() - serverLabel.text = "\(viewModel.selectedServerName): \(viewModel.selectedServerURL)" - - bindState(viewModel: viewModel) - bindActions(viewModel: viewModel) - } - -} - -// MARK: - Combine - -extension FetchViewController { - - func bindState(viewModel: FetchViewModel) { - viewModel.carResult - .sink { [weak self] in self?.handle(carResult: $0) } - .store(in: &cancellables) - - viewModel.productResult - .sink { [weak self] in self?.handle(productResult: $0) } - .store(in: &cancellables) - - viewModel.largeResult - .sink { [weak self] in self?.handle(largeResult: $0) } - .store(in: &cancellables) - } - - func bindActions(viewModel: FetchViewModel) { - fetchButton.publisher(for: .touchUpInside) - .sink { viewModel.fetchResponse() } - .store(in: &cancellables) - - fetchLarge.publisher(for: .touchUpInside) - .sink { viewModel.fetchLarge() } - .store(in: &cancellables) - } - -} - -// MARK: - Setup - -private extension FetchViewController { - - func setupLayout() { - view.backgroundColor = .white - navigationController?.navigationBar.prefersLargeTitles = true - title = Constants.Texts.Home.fetch - - addSubviews() - setupConstraints() - } - - func addSubviews() { - [responseLabel, serverLabel].forEach { stackView.addArrangedSubview($0) } - [stackView, fetchButton, fetchLarge].forEach { view.addSubview($0) } - } - - func setupConstraints() { - NSLayoutConstraint.activate([ - fetchButton.topAnchor.constraint(equalTo: fetchLarge.safeAreaLayoutGuide.bottomAnchor, constant: 20), - fetchButton.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -Constants.Insets.edgeInset * 2), - fetchButton.centerXAnchor.constraint(equalTo: view.centerXAnchor), - fetchLarge.centerXAnchor.constraint(equalTo: view.centerXAnchor), - - stackView.bottomAnchor.constraint(lessThanOrEqualTo: fetchButton.topAnchor, constant: -Constants.Insets.edgeInset), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Insets.edgeInset), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Insets.edgeInset), - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - ]) - } - -} - -// MARK: - Handle - -private extension FetchViewController { - - func handle(carResult: FetchViewModel.CarFetchingState) { - switch carResult { - case .idle: - responseLabel.text = Constants.Texts.Fetch.placeHolder - case .loading: - fetchButton.isLoading = true - case .success(let carResponse): - guard let data = try? JSONEncoder().encode(carResponse) else { return } - - fetchButton.isLoading = false - responseLabel.text = String(data: data, encoding: .utf8) - case .error(_): - responseLabel.text = nil - fetchButton.isLoading = false - } - } - - func handle(productResult: FetchViewModel.ProductFetchingState) { - switch productResult { - case .idle: - responseLabel.text = Constants.Texts.Fetch.placeHolder - case .loading: - fetchButton.isLoading = true - case .success(let productResponse): - guard let data = try? JSONEncoder().encode(productResponse) else { return } - - fetchButton.isLoading = false - responseLabel.text = String(data: data, encoding: .utf8) - case .error(_): - responseLabel.text = nil - fetchButton.isLoading = false - } - } - - func handle(largeResult: FetchViewModel.LargeFetchingState) { - switch largeResult { - case .idle: - responseLabel.text = Constants.Texts.Fetch.placeHolder - case .loading: - fetchLarge.isLoading = true - - case .success(let largeResponse): - guard let data = try? JSONEncoder().encode(largeResponse) else { return } - - fetchLarge.isLoading = false - - responseLabel.text = String(data: data, encoding: .utf8) - case .error(_): - responseLabel.text = nil - fetchLarge.isLoading = false - } - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewModel.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewModel.swift deleted file mode 100644 index e44efbc..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Fetch/FetchViewModel.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// HomeViewModel.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 14/09/2023. -// - -import Combine -import Alamofire -#if DEBUG -import AppDebugMode -#endif - -final class FetchViewModel { - - // MARK: - TypeAliases - - typealias DI = WithRequestManager - - // MARK: - Enums - - enum CarFetchingState { - - case idle - case loading - case success(CarResponse) - case error(AFError) - - } - - enum LargeFetchingState { - - case idle - case loading - case success(LargeObjectResponse) - case error(AFError) - - } - - enum ProductFetchingState { - - case idle - case loading - case success(ProductResponse) - case error(AFError) - - } - - // MARK: - Constants - - private let di: DI - private let coordinator: Coordinator - - // MARK: - Variables - Computed - - var selectedServerName: String { - #if DEBUG - return AppDebugModeProvider.shared.getSelectedServer(for: Constants.ServersCollections.sampleBackend).name - #else - return Constants.ProdServer.name - #endif - } - - var selectedServerURL: String { - #if DEBUG - return AppDebugModeProvider.shared.getSelectedServer(for: Constants.ServersCollections.sampleBackend).url - #else - return Constants.ProdServer.url - #endif - } - - // MARK: - Combine - - var largeResult = CurrentValueSubject(.idle) - var carResult = CurrentValueSubject(.idle) - var productResult = CurrentValueSubject(.idle) - private var cancellables = Set() - - // MARK: - Initializer - - init(di: DI, coordinator: Coordinator) { - self.coordinator = coordinator - self.di = di - } - -} - -// MARK: - Public - -extension FetchViewModel { - - func fetchResponse() { - #if DEBUG - selectedServerName == Constants.Servers.dev.name ? fetchProduct() : fetchCar() - #else - selectedServerName == Constants.DevServer.name ? fetchProduct() : fetchCar() - #endif - } - - func fetchLarge() { - di.requestManager.fetchLarge() - .map { LargeFetchingState.success($0) } - .catch { Just(.error($0)) } - .prepend(.loading) - .sink { [weak self] result in self?.largeResult.send(result) } - .store(in: &cancellables) - } - - func fetchCar() { - di.requestManager.fetchCars(id: Int.random(in: 1...10)) - .map { CarFetchingState.success($0) } - .catch { Just(.error($0)) } - .prepend(.loading) - .sink { [weak self] result in self?.carResult.send(result) } - .store(in: &cancellables) - } - - func fetchProduct() { - di.requestManager.fetchProducts(id: Int.random(in: 1...10)) - .map { ProductFetchingState.success($0) } - .catch { Just(.error($0)) } - .prepend(.loading) - .sink { [weak self] result in self?.productResult.send(result) } - .store(in: &cancellables) - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewController.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewController.swift index 36ca17a..2a1ac7b 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewController.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewController.swift @@ -5,116 +5,162 @@ // Created by Lukas Kubaliak on 20/09/2023. // +import AppDebugMode import UIKit -final class HomeViewController: BaseViewController { - - // MARK: - Constants +final class HomeViewController: UIViewController { + + // MARK: - Properties + + private let viewModel: HomeViewModel + + // MARK: - Initializer + + init(viewModel: HomeViewModel) { + self.viewModel = viewModel + + super.init(nibName: nil, bundle: nil) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + // MARK: - UI Components + private let stackView: UIStackView = { let stackView = UIStackView() stackView.translatesAutoresizingMaskIntoConstraints = false stackView.axis = .vertical stackView.spacing = 8 - + return stackView }() - + private let descriptionLabel: UILabel = { let label = UILabel() label.translatesAutoresizingMaskIntoConstraints = false - label.text = Constants.Texts.Home.description + label.text = "...allows an iOS application to select an API Server and User for the app during runtime" label.textColor = .secondaryLabel label.numberOfLines = 0 - + return label }() - - private let fetchModeButton: BasicButton = { + + private let userLoginModeButton: BasicButton = { let button = BasicButton() - button.setTitle(Constants.Texts.Home.fetch, for: .normal) + button.setTitle("User Login Mode", for: .normal) return button }() - private let loginModeButton: BasicButton = { + private let apiServerModeButton: BasicButton = { let button = BasicButton() - button.setTitle(Constants.Texts.Home.login, for: .normal) - + button.setTitle("API Server Mode", for: .normal) + return button }() - - private let userDefaultsModeButton: BasicButton = { + + private let userProfileModeButton: BasicButton = { let button = BasicButton() - button.setTitle(Constants.Texts.Home.userDefaults, for: .normal) - + button.setTitle("User Profile Mode", for: .normal) + return button }() - + + private let openAppDebugModeButton: BasicButton = { + let button = BasicButton() + button.setTitle("Open App Debug Mode", for: .normal) + return button + }() + } // MARK: - Lifecycle extension HomeViewController { - + override func viewDidLoad() { super.viewDidLoad() - + + bindActions() + setupLayout() - - bindAction(viewModel: viewModel) } - + } -// MARK: - Combine +// MARK: - Binding extension HomeViewController { - - func bindAction(viewModel: HomeViewModel) { - fetchModeButton.publisher(for: .touchUpInside) - .sink { viewModel.goToFetch() } - .store(in: &cancellables) - - loginModeButton.publisher(for: .touchUpInside) - .sink { viewModel.goToLogin() } - .store(in: &cancellables) - - userDefaultsModeButton.publisher(for: .touchUpInside) - .sink { viewModel.goToSettings()} - .store(in: &cancellables) + + func bindActions() { + userLoginModeButton.addTarget(self, action: #selector(userLoginModeButtonClicked), for: .touchUpInside) + apiServerModeButton.addTarget(self, action: #selector(apiServerModeButtonClicked), for: .touchUpInside) + userProfileModeButton.addTarget(self, action: #selector(userProfileModeButtonClicked), for: .touchUpInside) + #if DEBUG + openAppDebugModeButton.addTarget(self, action: #selector(openAppDebugModeButtonClicked), for: .touchUpInside) + #endif } - + + @objc func userLoginModeButtonClicked(_ sender: UIButton) { + Task { + await viewModel.goToUserLoginMode() + } + } + + @objc func apiServerModeButtonClicked(_ sender: UIButton) { + Task { + await viewModel.goToAPIServerMode() + } + } + + @objc func userProfileModeButtonClicked(_ sender: UIButton) { + Task { + await viewModel.goToUserProfileMode() + } + } + + #if DEBUG + @objc func openAppDebugModeButtonClicked(_ sender: UIButton) { + Task { + self.present(await AppDebugMode.PackageManager.shared.start(), animated: true) + } + } + #endif + } // MARK: - Setup private extension HomeViewController { - + func setupLayout() { view.backgroundColor = .white navigationController?.navigationBar.prefersLargeTitles = true - title = Constants.Texts.Home.title - + title = "App Debug Mode" + addSubviews() setupConstraints() } - + func addSubviews() { [descriptionLabel, stackView].forEach { view.addSubview($0) } - [loginModeButton, fetchModeButton, userDefaultsModeButton].forEach { stackView.addArrangedSubview($0) } + [userLoginModeButton, apiServerModeButton, userProfileModeButton, openAppDebugModeButton].forEach { + stackView.addArrangedSubview($0) + } } - + func setupConstraints() { NSLayoutConstraint.activate([ - descriptionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: Constants.Insets.edgeInset / 2), - descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Insets.edgeInset), - descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Insets.edgeInset), - + descriptionLabel.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 16 / 2), + descriptionLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + descriptionLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), + stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Insets.edgeInset), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Insets.edgeInset), + stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 16), + stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -16), ]) } - + } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewModel.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewModel.swift index 0348c38..2c176ea 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewModel.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Home/HomeViewModel.swift @@ -7,6 +7,7 @@ import Foundation +@MainActor final class HomeViewModel { // MARK: - Constants @@ -25,16 +26,16 @@ final class HomeViewModel { extension HomeViewModel { - func goToFetch() { - coordinator.navigate(to: .home(.goToFetch)) + func goToUserLoginMode() async { + await coordinator.navigate(to: .home(.goToUserLoginMode)) } - - func goToLogin() { - coordinator.navigate(to: .home(.goToLogin)) + + func goToAPIServerMode() async { + await coordinator.navigate(to: .home(.goToAPIServerMode)) } - - func goToSettings() { - coordinator.navigate(to: .home(.goToSettings)) + + func goToUserProfileMode() async { + await coordinator.navigate(to: .home(.goToUserProfile)) } } diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/LoginView.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/LoginView.swift new file mode 100644 index 0000000..7675149 --- /dev/null +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/LoginView.swift @@ -0,0 +1,70 @@ +// +// LoginView.swift +// AppDebugMode-iOS-Sample +// +// Created by Lukas Kubaliak on 20/09/2023. +// + +import SwiftUI +import Combine +#if DEBUG +import AppDebugMode +#endif + +struct LoginView: View { + + #if DEBUG + @AppStorage("selectedUserUser", store: UserDefaults(suiteName: AppDebugMode.Constants.suiteName)) + private(set) var selectedUserProfile: UserProfile? + #endif + + // MARK: - State + @State private var loginName: String = "" + @State private var password: String = "" + + // MARK: - Body + var body: some View { + NavigationView { + VStack(spacing: 16) { + + // Login Name Input + VStack(alignment: .leading) { + Text("Name") + .font(.caption) + TextField("Enter your login name", text: $loginName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .autocapitalization(.none) + .padding(.vertical, 5) + } + + // Password Input + VStack(alignment: .leading) { + Text("Password") + .font(.caption) + SecureField("Enter your login password", text: $password) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding(.vertical, 5) + } + + Spacer() + } + .padding(.horizontal, 32) + .navigationTitle("User Login Mode") + .navigationBarTitleDisplayMode(.large) + } + .onAppear() { + #if DEBUG + loginName = selectedUserProfile?.name ?? "No selected profile to use" + password = selectedUserProfile?.password ?? "No selected profile to use" + #else + loginName = "In DEV build value will be prefilled" + password = "In DEV build value will be prefilled" + #endif + } + } + +} + +#Preview { + LoginView() +} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewController.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewController.swift deleted file mode 100644 index 02b4c27..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewController.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// SettingsViewController.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 25/09/2023. -// - -import UIKit - -final class ProfileViewController: BaseViewController { - - // MARK: - Constants - - private let stackView: UIStackView = { - let stackView = UIStackView() - stackView.translatesAutoresizingMaskIntoConstraints = false - stackView.axis = .vertical - stackView.spacing = 16 - - return stackView - }() - - private let fullNameTextView: InputTextFieldView = { - let textView = InputTextFieldView() - textView.configure( - with: InputTextFieldView.Model( - title: Constants.Texts.Profile.nameTitle, - placeholder: Constants.Texts.Profile.namePlaceholder - ) - ) - - return textView - }() - - private let genderControl: UISegmentedControl = { - let segmentedControl = UISegmentedControl(items: Constants.Texts.Profile.genderOptions) - segmentedControl.selectedSegmentIndex = 0 - - return segmentedControl - }() - -} - -// MARK: - Lifecycle - -extension ProfileViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - fullNameTextView.textField.text = viewModel.loadFullName() - genderControl.selectedSegmentIndex = viewModel.loadGender() - - setupLayout() - - bindActions(viewModel: viewModel) - } - -} - -// MARK: - Selectors - -extension ProfileViewController { - - @objc func saveTapped(_ sender: UIButton) { - viewModel.saveUserData() - } - -} - -// MARK: - Combine - -extension ProfileViewController { - - func bindActions(viewModel: ProfileViewModel) { - fullNameTextView.textField.publisher(for: .editingChanged) - .sink { [weak self] _ in viewModel.fullName = self?.fullNameTextView.textField.text ?? "" } - .store(in: &cancellables) - - genderControl.publisher(for: .valueChanged) - .sink { [weak self] _ in - guard let self = self else { return } - - viewModel.gender = self.genderControl.selectedSegmentIndex - } - .store(in: &cancellables) - } - -} - -// MARK: - Private - -private extension ProfileViewController { - - func setupLayout() { - view.backgroundColor = .white - navigationController?.navigationBar.prefersLargeTitles = true - title = Constants.Texts.Home.userDefaults - - navigationItem.rightBarButtonItem = UIBarButtonItem(title: "Save", style: .done, target: self, action: #selector(saveTapped(_:))) - - addSubviews() - setupConstraints() - } - - func addSubviews() { - view.addSubview(stackView) - [fullNameTextView, genderControl].forEach { stackView.addArrangedSubview($0) } - } - - func setupConstraints() { - NSLayoutConstraint.activate([ - stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor), - stackView.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: Constants.Insets.edgeInset), - stackView.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -Constants.Insets.edgeInset) - ]) - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewModel.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewModel.swift deleted file mode 100644 index 760a16c..0000000 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/Profile/ProfileViewModel.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// SettingsViewModel.swift -// AppDebugMode-iOS-Sample -// -// Created by Lukas Kubaliak on 25/09/2023. -// - -import Foundation - -final class ProfileViewModel { - - // MARK: - TypeAliases - - typealias DI = WithCacheManager - - // MARK: - Constants - - private let di: DI - private let coordinator: Coordinator - - // MARK: - Variables - - var fullName = String() - var gender = Int() - - // MARK: - Initializer - - init(di: DI, coordinator: Coordinator) { - self.coordinator = coordinator - self.di = di - - fullName = loadFullName() ?? "" - } - -} - -// MARK: - Public - -extension ProfileViewModel { - - func saveUserData() { - di.cacheManager.updateFullName(with: fullName) - di.cacheManager.udpateGender(with: gender) - } - - func loadFullName() -> String? { - return di.cacheManager.fullName - } - - func loadGender() -> Int { - return di.cacheManager.gender - } - -} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ProfileView.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ProfileView.swift new file mode 100644 index 0000000..370f89d --- /dev/null +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Screens/ProfileView.swift @@ -0,0 +1,61 @@ +// +// ProfileView.swift +// AppDebugMode-iOS-Sample +// +// Created by Lukas Kubaliak on 25/09/2023. +// + +import SwiftUI + +struct ProfileView: View { + + // MARK: - AppStorage + + @AppStorage("fullName") var fullName: String = "" + @AppStorage("gender") var gender: Int = 0 + @AppStorage("favoriteColor") var favoriteColor: String = "Red" + + // MARK: - Body + + var body: some View { + NavigationView { + VStack(spacing: 16) { + TextField("Enter your full name", text: $fullName) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .padding() + .overlay( + VStack { + Text("Full name") + .font(.caption) + .padding(.horizontal) + .padding(.top, -30) + .padding(.bottom, 10) + }, + alignment: .leading + ) + + Picker("Gender", selection: $gender) { + Text("Male").tag(0) + Text("Female").tag(1) + } + .pickerStyle(SegmentedPickerStyle()) + .padding() + + HStack { + Text("Favorite Color(Preset):") + Text(favoriteColor) + } + .padding() + + Spacer() + } + .navigationTitle("User Profile Mode") + } + .padding() + } + +} + +#Preview { + ProfileView() +} diff --git a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Views/UIButton/FetchButton.swift b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Views/UIButton/FetchButton.swift index 7849c5a..92e5f18 100644 --- a/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Views/UIButton/FetchButton.swift +++ b/AppDebugMode-iOS-Sample/AppDebugMode-iOS-Sample/Views/UIButton/FetchButton.swift @@ -37,7 +37,7 @@ class FetchButton: UIButton { isUserInteractionEnabled = !isLoading } } - + override var isHighlighted: Bool { didSet { guard oldValue != isHighlighted else { return } @@ -60,7 +60,9 @@ class FetchButton: UIButton { override init(frame: CGRect) { super.init(frame: frame) - + + configuration = .bordered() + setupLayout() } @@ -92,6 +94,12 @@ private extension FetchButton { heightAnchor.constraint(equalToConstant: C.height).isActive = true titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold) setTitleColor(.black, for: .normal) + backgroundColor = .white + layer.borderWidth = 1 + layer.borderColor = UIColor.black.cgColor + layer.cornerRadius = C.height / 2 + layer.cornerCurve = .continuous + layer.masksToBounds = true } func startLoading() { diff --git a/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.resolved b/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.resolved deleted file mode 100644 index 3add499..0000000 --- a/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.resolved +++ /dev/null @@ -1,113 +0,0 @@ -{ - "pins" : [ - { - "identity" : "alamofire", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/Alamofire.git", - "state" : { - "revision" : "78424be314842833c04bc3bef5b72e85fff99204", - "version" : "5.6.4" - } - }, - { - "identity" : "alamofireimage", - "kind" : "remoteSourceControl", - "location" : "https://github.com/Alamofire/AlamofireImage.git", - "state" : { - "revision" : "98cbb00ce0ec5fc8e52a5b50a6bfc08d3e5aee10", - "version" : "4.2.0" - } - }, - { - "identity" : "combineext", - "kind" : "remoteSourceControl", - "location" : "https://github.com/CombineCommunity/CombineExt.git", - "state" : { - "revision" : "d7b896fa9ca8b47fa7bcde6b43ef9b70bf8c1f56", - "version" : "1.8.1" - } - }, - { - "identity" : "deepdiff", - "kind" : "remoteSourceControl", - "location" : "https://github.com/onmyway133/DeepDiff.git", - "state" : { - "revision" : "9d5c67c58b9f06c0c638bd98ee4093abbe4a08a1", - "version" : "2.3.0" - } - }, - { - "identity" : "goodextensions-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GoodRequest/GoodExtensions-iOS.git", - "state" : { - "revision" : "99883629415b81f794e71284d60985c9796d4292", - "version" : "1.1.0" - } - }, - { - "identity" : "goodnetworking", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GoodRequest/GoodNetworking.git", - "state" : { - "revision" : "dab47f85dbcaac2fec6c7c616d0d77152c50a3e2", - "version" : "1.0.1" - } - }, - { - "identity" : "goodpersistence", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GoodRequest/GoodPersistence.git", - "state" : { - "revision" : "780e03a1e21948b114d7af3bc0cfe0d8aecae4c0", - "version" : "1.0.2" - } - }, - { - "identity" : "goodreactor", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GoodRequest/GoodReactor.git", - "state" : { - "revision" : "eabaa34fd41d8bd9c9e5c7d4f11e96d4780243f5", - "version" : "1.0.1" - } - }, - { - "identity" : "gooduikit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GoodRequest/GoodUIKit.git", - "state" : { - "revision" : "67875ce4e0b07947020e3e5dceab57ffbc085c05", - "version" : "1.0.1" - } - }, - { - "identity" : "grprovider", - "kind" : "remoteSourceControl", - "location" : "https://github.com/GoodRequest/GRProvider.git", - "state" : { - "revision" : "2edf5f5abdfe4c799c6afc4d95c980dacd4749f5", - "version" : "0.1.3" - } - }, - { - "identity" : "logen", - "kind" : "remoteSourceControl", - "location" : "git@github.com:GoodRequest/Logen.git", - "state" : { - "revision" : "0d08bc50dd30a8422bc749b73fbe667d04d59297", - "version" : "1.0.0" - } - }, - { - "identity" : "opsfiles-ios", - "kind" : "remoteSourceControl", - "location" : "git@github.com:GoodRequest/OpsFiles-iOS.git", - "state" : { - "revision" : "402f88664aba44d53996f626b01010a8964aa989", - "version" : "1.0.0" - } - } - ], - "version" : 2 -} diff --git a/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.swift b/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.swift index 9546686..bb3ac65 100644 --- a/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.swift +++ b/AppDebugMode-iOS-Sample/Packages/GoodDependencies/Package.swift @@ -1,10 +1,11 @@ -// swift-tools-version:5.5 +// swift-tools-version: 6.0 +// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "GoodDependencies", - platforms: [.iOS(.v14)], + platforms: [.iOS(.v15)], products: [ .library( name: "GoodDependencies", @@ -12,14 +13,15 @@ let package = Package( ), ], dependencies: [ - .package(url: "https://github.com/GoodRequest/GoodNetworking.git", .upToNextMajor(from: "2.0.0")) + .package(url: "https://github.com/GoodRequest/GoodNetworking.git", .upToNextMajor(from: "3.0.0")), + .package(url: "https://github.com/GoodRequest/GoodLogger.git", .upToNextMajor(from: "1.2.1")) ], targets: [ .target( name: "GoodDependenciesTarget", dependencies: [ .product(name: "GoodNetworking", package: "GoodNetworking"), - .product(name: "Mockable", package: "GoodNetworking") + .product(name: "GoodLogger", package: "GoodLogger") ], path: "" ) diff --git a/Package.resolved b/Package.resolved index aef9319..189d6c4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,4 +1,5 @@ { + "originHash" : "a63a34508ef36d36d0424924512a97dfd81aa1689a7dc0c6eed9c4d38859b835", "pins" : [ { "identity" : "combineext", @@ -9,13 +10,31 @@ "version" : "1.8.1" } }, + { + "identity" : "factory", + "kind" : "remoteSourceControl", + "location" : "https://github.com/hmlongco/Factory.git", + "state" : { + "revision" : "587995f7d5cc667951d635fbf6b4252324ba0439", + "version" : "2.3.2" + } + }, + { + "identity" : "goodlogger", + "kind" : "remoteSourceControl", + "location" : "https://github.com/GoodRequest/GoodLogger.git", + "state" : { + "revision" : "7e0a11ffa920889c8d289c1dca60d6b0c94b0ae9", + "version" : "1.0.0" + } + }, { "identity" : "goodpersistence", "kind" : "remoteSourceControl", "location" : "https://github.com/GoodRequest/GoodPersistence.git", "state" : { - "revision" : "841f777b44a6061e3a6f4e01ffb1e36af2dfe13f", - "version" : "2.1.0" + "revision" : "43ea33ee9a4cd19094e8eeeb007c58f94c2f8e23", + "version" : "2.3.0" } }, { @@ -32,10 +51,10 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/siteline/swiftui-introspect", "state" : { - "revision" : "0cd2a5a5895306bc21d54a2254302d24a9a571e4", - "version" : "1.1.3" + "revision" : "807f73ce09a9b9723f12385e592b4e0aaebd3336", + "version" : "1.3.0" } } ], - "version" : 2 + "version" : 3 } diff --git a/Package.swift b/Package.swift index 26d3ce3..590f0ee 100644 --- a/Package.swift +++ b/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version: 5.8 +// swift-tools-version: 6.0 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "AppDebugMode", - platforms: [.iOS(.v14)], + platforms: [.iOS(.v15)], products: [ // Products define the executables and libraries a package produces, and make them visible to other packages. .library( @@ -13,8 +13,11 @@ let package = Package( targets: ["AppDebugMode"]), ], dependencies: [ - .package(url: "https://github.com/GoodRequest/GoodPersistence.git", .upToNextMajor(from: "2.1.0")), - .package(url: "https://github.com/siteline/swiftui-introspect", from: "1.0.0"), + .package(url: "https://github.com/kean/Pulse.git", .upToNextMajor(from: "5.1.1")), + .package(url: "https://github.com/siteline/swiftui-introspect.git", .upToNextMajor(from: "1.3.0")), + .package(url: "https://github.com/hmlongco/Factory.git", from: "2.0.0"), + .package(url: "https://github.com/GoodRequest/GoodNetworking.git", .upToNextMajor(from: "3.0.0")), + .package(url: "https://github.com/GoodRequest/GoodLogger.git", .upToNextMajor(from: "1.2.1")) ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -22,14 +25,22 @@ let package = Package( .target( name: "AppDebugMode", dependencies: [ - .product(name: "GoodPersistence", package: "GoodPersistence"), + .product(name: "Pulse", package: "Pulse"), + .product(name: "PulseUI", package: "Pulse"), + .product(name: "PulseProxy", package: "Pulse"), .product(name: "SwiftUIIntrospect", package: "swiftui-introspect"), + .product(name: "Factory", package: "Factory"), + .product(name: "GoodNetworking", package: "GoodNetworking"), + .product(name: "GoodLogger", package: "GoodLogger") ], path: "Sources", - resources: [.copy("PrivacyInfo.xcprivacy")] + resources: [.copy("PrivacyInfo.xcprivacy")], + swiftSettings: [.swiftLanguageMode(.v6)] ), .testTarget( name: "AppDebugModeTests", - dependencies: ["AppDebugMode"]), + dependencies: ["AppDebugMode"], + swiftSettings: [.swiftLanguageMode(.v6)] + ), ] ) diff --git a/Sources/AppDebugMode/Utils/StandardOutputService.swift b/Sources/AppDebugMode/Dependencies/Logging/StandardOutput.swift similarity index 55% rename from Sources/AppDebugMode/Utils/StandardOutputService.swift rename to Sources/AppDebugMode/Dependencies/Logging/StandardOutput.swift index 8bdf0c2..ef9c760 100644 --- a/Sources/AppDebugMode/Utils/StandardOutputService.swift +++ b/Sources/AppDebugMode/Dependencies/Logging/StandardOutput.swift @@ -1,23 +1,15 @@ // -// StandardOutputService.swift +// StandardOutput.swift +// AppDebugMode // +// Created by Andrej Jasso on 16/09/2024. // -// Created by Andrej Jasso on 29/01/2024. -// - -import SwiftUI -import GoodPersistence -import Combine - -final public class StandardOutputService: ObservableObject { - // MARK: - Singleton +import Foundation - static public var shared = StandardOutputService() - - static let testService: StandardOutputService = { - let service = StandardOutputService() - let sampleValues = [ +struct StandardOutput { + + static let sampleValues = [ """ 🚀GET| ✅200| https://myfakeapi.com/api/cars/Top 🏷 Headers: empty headers @@ -81,97 +73,8 @@ final public class StandardOutputService: ObservableObject { "🚀GET| ✅200| https://myfakeapi.com/api/cars/4/5353afeafaef4faf4f4fa4fgafa4t5y5r65eh5eh5eh5eh5eh5hrfthfthftthfthft", "🚀GET| ✅200| https://myfakeapi.com/api/cars/4/5353afeafaef4faf4f4fa4fgafa4t5y5r65eh5eh5eh5eh5eh5hrfthfthftthfthft", "🚀GET| ✅200| https://myfakeapi.com/api/cars/4/Bottom" - ].map { - Log(message: $0) - } - service.capturedOutput = sampleValues - return service - }() - - // MARK: - Log - - struct Log: Identifiable { - - var id: UInt64 { - absoluteSystemTime - } - - let message: String - let date = Date() - - private let absoluteSystemTime: UInt64 = mach_absolute_time() - + ].map { + Log(message: $0) } - - // MARK: - Variables - - @Published var capturedOutput: [Log] = [] - - // MARK: - Private - - private var cancellables: Set = [] - private var didRedirectLogs: Bool = false - private var pipe = Pipe() - private var count = 0 - - @UserDefaultValue("shouldRedirectLogsToAppDebugMode", defaultValue: !DebuggerService.debuggerConnected()) - var shouldRedirectLogsToAppDebugMode: Bool - - // MARK: - Helper functions - - func redirectLogsToAppDebugMode () { - guard !didRedirectLogs else { return } // redirect only once - didRedirectLogs = true - - setvbuf(stdout, nil, _IONBF, 0) // set output as unbuffered - dup2(pipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) - pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in - let data = handle.availableData - let readString = String(data: data, encoding: .utf8) ?? "<\(data.count) bytes of non-UTF-8 data>\n" - - self?.redirectLog(readString) - } - - //OSLog/Console logs are written to stderr - dup2(pipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO) - pipe.fileHandleForReading.readabilityHandler = { [weak self] handle in - let data = handle.availableData - var readString = String(data: data, encoding: .utf8) ?? "<\(data.count) bytes of non-UTF-8 data>\n" - - // Trim OSLog metadata prefix - if let regex = try? NSRegularExpression(pattern: #"^(\nOSLOG-.*?\t)"#, options: [.anchorsMatchLines]), - let match = regex.firstMatch(in: readString, options: [], range: NSRange(location: 0, length: readString.utf16.count)), - let range = Range(match.range, in: readString) { - readString = readString.replacingCharacters(in: range, with: "") - - } - - self?.redirectLog(readString) - } - } - - public func connectCustomLogStreamPublisher(_ stream: AnyPublisher) { - stream.sink { logMessage in - // Send message to 🪲 Debugman - AppDebugModeProvider.shared.connectionManager.sendToAllPeers(message: logMessage) - let log = StandardOutputService.Log(message: logMessage) - StandardOutputService.shared.capturedOutput.append(log) - } - .store(in: &cancellables) - } - - func redirectLog(_ string: String) { - DispatchQueue.main.async { [weak self] in - let log = Log(message: string) - guard !log.message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { - return - } - self?.capturedOutput.append(log) - } - } - - func clearLogs() { - capturedOutput.removeAll() - } - + } diff --git a/Sources/AppDebugMode/Dependencies/Logging/StandardOutputProcessor.swift b/Sources/AppDebugMode/Dependencies/Logging/StandardOutputProcessor.swift new file mode 100644 index 0000000..aa3a54e --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/Logging/StandardOutputProcessor.swift @@ -0,0 +1,76 @@ +// +// StandardOutputService.swift +// +// Created by Andrej Jasso on 29/01/2024. +// + +import SwiftUI + +public actor StandardOutputProcessor: ObservableObject { + + public static let shared = StandardOutputProcessor() + + // MARK: - Properties + + private var didRedirectLogs = false + private let stdOutPipe = Pipe() + private let stdErrPipe = Pipe() + private let count = 0 + + @AppStorage("shouldRedirectLogsToAppDebugMode", store: UserDefaults(suiteName: Constants.suiteName)) + var shouldRedirectLogsToAppDebugMode = !DebuggerService.debuggerConnected() { + didSet { + if shouldRedirectLogsToAppDebugMode { + redirectLogsToAppDebugMode() + } + } + } + + @AppStorage("capturedOutput", store: UserDefaults(suiteName: Constants.suiteName)) + var capturedOutput: [Log] = [] + + // MARK: - Helper functions + + func redirectLogsToAppDebugMode () { + guard !didRedirectLogs else { return } // redirect only once + didRedirectLogs = true + + setvbuf(stdout, nil, _IONBF, 0) // set output as unbuffered + dup2(stdOutPipe.fileHandleForWriting.fileDescriptor, STDOUT_FILENO) + stdOutPipe.fileHandleForReading.readabilityHandler = { [weak self] handler in + let readString = String(data: handler.availableData, encoding: .utf8) ?? "<\(handler.availableData.count) bytes of non-UTF-8 data>\n" + Task { + await self?.redirectLog(readString) + } + } + + setvbuf(stderr, nil, _IONBF, 0) // set output as unbuffered + dup2(stdErrPipe.fileHandleForWriting.fileDescriptor, STDERR_FILENO) + stdErrPipe.fileHandleForReading.readabilityHandler = { [weak self] handle in + let data = handle.availableData + var readString = String(data: data, encoding: .utf8) ?? "<\(data.count) bytes of non-UTF-8 data>\n" + + // Trim OSLog metadata prefix + if let regex = try? NSRegularExpression(pattern: #"^(\nOSLOG-.*?\t)"#, options: [.anchorsMatchLines]), + let match = regex.firstMatch(in: readString, options: [], range: NSRange(location: 0, length: readString.utf16.count)), + let range = Range(match.range, in: readString) { + readString = readString.replacingCharacters(in: range, with: "") + + } + Task { + await self?.redirectLog(readString) + } + } + } + + func redirectLog(_ string: String) async { + if shouldRedirectLogsToAppDebugMode { + let log = Log(message: string) + guard !log.message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty else { + return + } + self.capturedOutput.append(log) + } + } + +} diff --git a/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/DebugManSessionManager.swift b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/DebugManSessionManager.swift new file mode 100644 index 0000000..8bb7263 --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/DebugManSessionManager.swift @@ -0,0 +1,521 @@ +// +// SessionManager.swift +// +// +// Created by Matus Klasovity on 21/05/2024. +// + +import Combine +import MultipeerConnectivity +import Factory +import SwiftUI + +@MainActor +final class DebugManSessionManager: ObservableObject { + + // MARK: - Factory + + @Injected(\.proxySettingsProvider) private var proxyProvider: ProxySettingsProvider + @Injected(\.sessionDelegateWrapper) private var sessionDelegateWrapper: PeerSessionDelegateWrapper + @LazyInjected(\.nearbyServicesBrowserDelegate) private var nearbyServicesDelegateWrapper: PeerBrowserDelegateWrapper + + // MARK: - State + + var isSessionDisconnected = false + + private var errorStreamContinuation: AsyncStream.Continuation? + + func errorStream() -> AsyncStream { + return AsyncStream { continuation in + self.errorStreamContinuation = continuation + } + } + + // MARK: - Cache + + @AppStorage("remotePeer", store: UserDefaults(suiteName: Constants.suiteName)) + var remotePeer: Peer? + + @AppStorage("localPeer", store: UserDefaults(suiteName: Constants.suiteName)) + var localPeer: Peer = .defaultPeer + + @Published var debugManConnectionState: DebugManConnectionState = .clean { + didSet { + print("📊 State changed to \(debugManConnectionState.rawValue)") + } + } + + @Published var mitmProxyState: ProxyConfigurationState = .clean + + @AppStorage("proxyIpAddress", store: UserDefaults(suiteName: "AppDebugMode")) + public var proxyIpAddress: String = "" + + @AppStorage("proxyPort", store: UserDefaults(suiteName: "AppDebugMode")) + public var proxyPort: Int = 8080 + + @AppStorage("isProxyValidated", store: UserDefaults(suiteName: "AppDebugMode")) + public var isProxyValidated = false + + // MARK: - Properties + + let userDefaultsSuite = UserDefaults(suiteName: Constants.suiteName) + + lazy var session: MCSession = { + // Session is needed every time since messages are sent for logging there + + let session = MCSession( + peer: localPeer.asPeerId(), + securityIdentity: nil, + encryptionPreference: .none + ) + + return session + }() + + lazy var browser: MCNearbyServiceBrowser = { + let browser = MCNearbyServiceBrowser( + peer: localPeer.asPeerId(), + serviceType: "Debugman" + ) + + return browser + }() + + var peers: Set = [] + + var error: (any Error)? = nil { + didSet { + if let error { + errorStreamContinuation?.yield(error) + } + } + } + + // MARK: - Initializer + + nonisolated + init() {} + + func start() async { + + if localPeer == .defaultPeer { + let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String ?? "AppDebugMode" + let newDisplayName = "\(appName) (\(Int.random(in: 10000...99999)))" + print("👀 Creating new local peer \(newDisplayName)") + localPeer = Peer(peerId: MCPeerID(displayName: newDisplayName)) + } + + if !proxyIpAddress.isEmpty, isProxyValidated { + mitmProxyState = .configured(configuration: ProxyConfiguration(ipAddresses: [proxyIpAddress], port: UInt16(proxyPort))) + } + + session.delegate = sessionDelegateWrapper + + startListeningToSessionStateUpdates() + startListeningToPacketUpdates() + + // If remote peer is not nil we are already have an peer to connect to + if let remotePeer { + self.debugManConnectionState = .disconnected(from: remotePeer) + do { + try await reconnectRemotePeer() + } catch { + self.error = error + } + } else { + // If state is clean we will likely need services brower to look for devices + self.debugManConnectionState = .clean + } + } + + func startListeningToSessionStateUpdates() { + print("👂📊 Starting to listen to session updates") + Task { + for await update in sessionDelegateWrapper.sessionStateStream() { + handleState(mcSessionState: update.state, peer: update.peer) + } + } + } + + func startListeningToPacketUpdates() { + print("👂📦 Starting to listen to packets") + Task { + do { + for try await packet in sessionDelegateWrapper.peerMessagesStream() { + await handleIncomingData(packet: packet) + } + } catch { + errorStreamContinuation?.yield(error) + } + } + } + +} + +// MARK: Public + +extension DebugManSessionManager { + + func send(_ message: String) throws(PeerSessionError) { + guard let connectedPeer = remotePeer?.asPeerId() else { + throw .sendingLogsFailed + } + + let data = Data(message.utf8) + + print("💬 Sending message: \(message)") + + do { + try session.send( + data, + toPeers: [connectedPeer], + with: .reliable + ) + } catch { + print("🔥 Error sending data to peers: \(error)") + errorStreamContinuation?.yield(error) + } + } + + func cleanConfig() { + proxyProvider.clean() + mitmProxyState = .clean + isProxyValidated = false + print("📜 Config Cleaned") + } + + + func unpair() throws(PeerSessionError) { + print("📜 Unpairing \(String(describing: remotePeer?.asPeerId().displayName))") + + setState(.clean) + self.remotePeer = nil + } + + func cleanState() { + browser.stopBrowsingForPeers() + session.disconnect() + cleanConfig() + print("📜 Tabula Rasa") + } + + func startBrowsing() throws(PeerSessionError) { + guard debugManConnectionState == .clean else { + throw .browsingBlocked + } + if browser.delegate == nil { + browser.delegate = nearbyServicesDelegateWrapper + } else { + print("delegate already in place") + } + browser.delegate = nearbyServicesDelegateWrapper + + print("👀 Starting to browse for peers on the network ...") + setState(.browsing) + browser.startBrowsingForPeers() + } + + + func invite(peer: Peer, timeout: TimeInterval) throws(PeerSessionError) { + guard debugManConnectionState == .browsing else { + throw .invitingBlocked + } + + print("💌 Inviting peer: \(peer.asPeerId().displayName)") + setState(.waitingForAccept(from: peer)) + + browser.invitePeer( + peer.asPeerId(), + to: session, + withContext: nil, + timeout: TimeInterval(timeout) + ) + } + + func stopBrowsing() throws(PeerSessionError) { + guard debugManConnectionState == .browsing else { + throw .stopBrowsingBlocked + } + + browser.stopBrowsingForPeers() + } + + func disconnect() throws(PeerSessionError) { + guard debugManConnectionState.isConnected else { + throw .disconnectBlocked + } + session.disconnect() + isSessionDisconnected = true + } + + func reconnectRemotePeer() async throws(PeerSessionError) { + guard debugManConnectionState.isDisconnected else { + throw .reconnectingBlocked + } + + + guard let remotePeer else { + throw .reconnectingBlocked + } + + setState(.connecting(to: remotePeer)) + + if isSessionDisconnected { + let session = MCSession( + peer: localPeer.asPeerId(), + securityIdentity: nil, + encryptionPreference: .none + ) + self.session = session + session.delegate = sessionDelegateWrapper + startListeningToSessionStateUpdates() + isSessionDisconnected = false + } + + if browser.delegate == nil { + browser.delegate = nearbyServicesDelegateWrapper + } else { + print("delegate already in place") + } + + browser.startBrowsingForPeers() + + guard let onlineRemotePeer = try? await waitForPeer( + with: remotePeer, + timeout: 10, + wrapper: nearbyServicesDelegateWrapper + ) else { + throw .reconnectingPeerNotFound + } + + browser.invitePeer( + onlineRemotePeer.asPeerId(), + to: session, + withContext: nil, + timeout: 10 + ) + + browser.stopBrowsingForPeers() + } + +} + +// MARK: - MCSessionDelegate + +extension DebugManSessionManager { + + func handleState(mcSessionState: PeerConnectionState, peer: Peer) { + print("🛜 MCSession state \(mcSessionState) to peer: \(peer.name)") + switch mcSessionState { + case .notConnected: + setState(.disconnected(from: peer)) + + case .connecting: + setState(.connecting(to: peer)) + + case .connected: + self.remotePeer = peer + setState(.connected(to: peer)) + } + } + +} + +// MARK: - Helper functions - private + +private extension DebugManSessionManager { + + func waitForPeer(with peer: Peer, timeout: TimeInterval, wrapper: PeerBrowserDelegateWrapper) async throws -> Peer { + async let timeoutTask: () = Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) + + while true { + if let awaitedPeer = wrapper.peers.first(where: { $0 == peer }) { + return awaitedPeer + } + + try? await Task.sleep(nanoseconds: 100_000_000) + try await timeoutTask + } + + throw MCError(.timedOut) + } + + func send(_ packet: JSONPacket) throws(ProxyConfigurationError) { + guard + debugManConnectionState.isConnected, + let remotePeer else + { + throw .packetSendBlocked + } + + let encoder = JSONEncoder() + do { + let data = try encoder.encode(packet) + print("📦 Sending packet: \(packet)") + + try session.send( + data, + toPeers: [remotePeer.asPeerId()], + with: .reliable + ) + } catch { + throw .encodingPacketFailed + } + } + + func setState(_ newState: DebugManConnectionState) { + self.debugManConnectionState = newState + } + +} + +// MARK: - Proxy Confguration Handling + +extension DebugManSessionManager { + + func handleIncomingData(packet: JSONPacket) async { + switch packet { + case .proxyConfiguration(let proxyConfiguration): + do { + try await handleNewProxyConfiguration(proxyConfiguration) + } catch { + self.error = error + } + + case .certificateRequest: + errorStreamContinuation?.yield(ProxyConfigurationError.requestingCertificateFromAppDebugMode) + + case .requireCertificateTrust, .requestProxyConfiguration: + break + + case .pairingSuccessful: + print("🎊 TADA") + } + } + + func setProxyState(proxyState: ProxyConfigurationState) { + print("⚙️ Proxy State changed to \(proxyState.title)") + self.mitmProxyState = proxyState + } + + // Attempts to validate new configuration again MITMProxy server to validate certificates with IPAdress and port + func handleNewProxyConfiguration(_ proxyConfiguration: ProxyConfiguration) async throws(ProxyConfigurationError) { + setProxyState(proxyState: .testing(configuration: proxyConfiguration)) + + #warning("Add saving first configuration") + #warning("Add requesting first configuration again") + + let port = proxyConfiguration.port + var didAnyIpPass = false + + for ipAddress in proxyConfiguration.ipAddresses { + proxyProvider.updateProxySettings(ipAddress: ipAddress, port: port) + switch await proxyProvider.testProxyAt(ipAddress: ipAddress, port: port) { + case .connectionFailed: + setProxyState(proxyState: .waitingForUserToInstalCertificate) + throw .testingCertificateConnectionFailed + + case .missingCertificate: + setProxyState(proxyState: .waitingForUserToInstalCertificate) + try await requireCertificateTrust() + + case .success: + didAnyIpPass = true + setProxyState(proxyState: .configured(configuration: proxyConfiguration)) + proxyProvider.updateProxySettings(ipAddress: ipAddress, port: port) + } + } + + if !didAnyIpPass { + setProxyState(proxyState: .waitingForUserToTrustMITMProxy) + print("Error: No IP addresses passed the test. Require Certificate Trust.") + } + } + + func validateSavedProxyConfiguration() async throws(ProxyConfigurationError) { + let configuration = ProxyConfiguration(ipAddresses: [proxyProvider.proxyIpAddress], port: proxyProvider.proxyPortUInt16) + setProxyState(proxyState: .testing(configuration: configuration)) + let result = await proxyProvider.testProxyAt(ipAddress: proxyIpAddress, port: UInt16(proxyPort)) + + switch result { + case .success: + try await proxyConfigurationSuccessful() + setProxyState(proxyState: .configured(configuration: configuration)) + + case .missingCertificate: + setProxyState(proxyState: .waitingForUserToInstalCertificate) + try await requireCertificateTrust() + + case .connectionFailed: + setProxyState(proxyState: .waitingForUserToInstalCertificate) + throw .testingCertificateConnectionFailed + } + } + + func proxyConfigurationSuccessful() async throws(ProxyConfigurationError) { + guard debugManConnectionState.isConnected, + mitmProxyState.isTesting + else { + throw .sendingSuccessBlocked + } + try send(.pairingSuccessful) + } + + func requestConfiguration() async throws(ProxyConfigurationError) { + guard debugManConnectionState.isConnected else { + throw .requestingProxyConfigurationBlocked + } + + setProxyState(proxyState: .waitingForUserToInstalCertificate) + + do { + try send(.requestProxyConfiguration) + } catch { + throw error + } + } + + + func requestNewCertificate() async throws(ProxyConfigurationError) { + guard debugManConnectionState.isConnected, + !mitmProxyState.isConfigured else { + throw .requestingNewCertificateBlocked + } + + setProxyState(proxyState: .waitingForUserToInstalCertificate) + + do { + try send(.certificateRequest) + } catch { + throw error + } + } + + func requireCertificateTrust() async throws(ProxyConfigurationError) { + guard debugManConnectionState.isConnected else { + throw .requestingCertificateTrustBlocked + } + + setProxyState(proxyState: .waitingForUserToTrustMITMProxy) + + do { + try send(.requireCertificateTrust) + } catch { + throw error + } + } + + func requestNewConfiguration() async throws(ProxyConfigurationError) { + guard debugManConnectionState.isConnected else { + throw .requestingProxyConfigurationBlocked + } + + do { + try send(.requestProxyConfiguration) + } catch { + throw error + } + } + + +} diff --git a/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerBrowserDelegateWrapper.swift b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerBrowserDelegateWrapper.swift new file mode 100644 index 0000000..2cb25cc --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerBrowserDelegateWrapper.swift @@ -0,0 +1,60 @@ +// +// PeerBrowserDelegateWrapper.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +import MultipeerConnectivity + +final class PeerBrowserDelegateWrapper: NSObject { + + var peers: Set = [] + + // MARK: - Streams + + private var peerDiscoveryStreamContinuation: AsyncThrowingStream, Error>.Continuation? + + public func peerDiscoveryStream() -> AsyncThrowingStream, Error> { + return AsyncThrowingStream { continuation in + self.peerDiscoveryStreamContinuation = continuation + } + } + + func stopPeerDiscoveryStream() { + peerDiscoveryStreamContinuation?.finish() + } + +} + +extension PeerBrowserDelegateWrapper: MCNearbyServiceBrowserDelegate { + + func browser(_ browser: MCNearbyServiceBrowser, didNotStartBrowsingForPeers error: any Error) { + print("❌ [MCNearbyServiceBrowserDelegate] didNotStartBrowsingForPeers \(error.localizedDescription)") + peerDiscoveryStreamContinuation?.yield(with: .failure(error)) + stopPeerDiscoveryStream() + browser.stopBrowsingForPeers() + } + + func browser( + _ browser: MCNearbyServiceBrowser, + foundPeer peerID: MCPeerID, + withDiscoveryInfo info: [String: String]? + ) { + if info?["pairing"] == "YES" { + print("🛜 [MCNearbyServiceBrowserDelegate] foundPeer for pairing \(peerID.displayName)") + peers.insert(Peer(peerId: peerID)) + peerDiscoveryStreamContinuation?.yield(with: .success(peers)) + } else { + print("🛜 [MCNearbyServiceBrowserDelegate] foundPeer \(peerID.displayName)") + } + } + + func browser(_ browser: MCNearbyServiceBrowser, lostPeer peerID: MCPeerID) { + peers.remove(Peer(peerId: peerID)) + peerDiscoveryStreamContinuation?.yield(with: .success(peers)) + peerDiscoveryStreamContinuation?.yield(peers) + print("🛜 [MCNearbyServiceBrowserDelegate] lostPeer \(peerID.displayName)") + } + +} diff --git a/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerSessionDelegateWrapper.swift b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerSessionDelegateWrapper.swift new file mode 100644 index 0000000..2f00fb8 --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/PeerSessionDelegateWrapper.swift @@ -0,0 +1,88 @@ +// +// PeerSessionDelegateWrapper.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +import MultipeerConnectivity +import SwiftUI + +// MARK: - MCSessionDelegate + +final class PeerSessionDelegateWrapper: NSObject { + + // MARK: - Initializer + + override init() {} + + // MARK: - Streams + + private var sessionStateStreamContinuation: AsyncStream<(peer: Peer, state: PeerConnectionState)>.Continuation? + + func sessionStateStream() -> AsyncStream<(peer: Peer, state: PeerConnectionState)> { + return AsyncStream { continuation in + self.sessionStateStreamContinuation = continuation + } + } + + private var peerMessagesStreamContinuation: AsyncThrowingStream.Continuation? + + func peerMessagesStream() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + self.peerMessagesStreamContinuation = continuation + } + } + + func stopSessionStateStream() { + sessionStateStreamContinuation?.finish() + } + + func stopPeerMessageStream() { + peerMessagesStreamContinuation?.finish() + } + +} + +// MARK: - Delegate + +extension PeerSessionDelegateWrapper: MCSessionDelegate { + + func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { + sessionStateStreamContinuation?.yield((peer: Peer(peerId: peerID), state: PeerConnectionState(mcSessionState: state))) + } + + func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { + print("⬇️ received data from peer: \(peerID) data: \(String(describing: String(data: data, encoding: .utf8)))") + do { + let decoder = JSONDecoder() + let jsonPacket = try decoder.decode(JSONPacket.self, from: data) + peerMessagesStreamContinuation?.yield(with: .success(jsonPacket)) + } catch let error { + peerMessagesStreamContinuation?.yield(with: .failure(error)) + } + } + + func session( + _ session: MCSession, + didReceive stream: InputStream, + withName streamName: String, + fromPeer peerID: MCPeerID + ) {} + + func session( + _ session: MCSession, + didStartReceivingResourceWithName resourceName: String, + fromPeer peerID: MCPeerID, + with progress: Progress + ) {} + + func session( + _ session: MCSession, + didFinishReceivingResourceWithName resourceName: String, + fromPeer peerID: MCPeerID, + at localURL: URL?, + withError error: (any Error)? + ) {} + +} diff --git a/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/ProxySettingsProvider.swift b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/ProxySettingsProvider.swift new file mode 100644 index 0000000..c6b356b --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/MultipeerConnectivity/ProxySettingsProvider.swift @@ -0,0 +1,106 @@ +// +// ProxySettingsProvider.swift +// +// +// Created by Matus Klasovity on 28/05/2024. +// + +import SwiftUI +import Factory +import GoodNetworking + +@MainActor +public final class ProxySettingsProvider: Sendable { + + // MARK: - Singleton + + public static let shared = ProxySettingsProvider() + + // MARK: - Properties + + @AppStorage("proxyIpAddress", store: UserDefaults(suiteName: "AppDebugMode")) + public var proxyIpAddress: String = "" + + @AppStorage("proxyPort", store: UserDefaults(suiteName: "AppDebugMode")) + public var proxyPort: Int = 8080 + + @AppStorage("isProxyValidated", store: UserDefaults(suiteName: "AppDebugMode")) + public var isProxyValidated = false + + // MARK: - Computed Property + + var proxyPortUInt16: UInt16 { + get { + return UInt16(proxyPort) + } + set { + proxyPort = Int(newValue) + } + } + + // MARK: - Public + + public func urlSessionConfiguration() -> URLSessionConfiguration { + urlSessionConfiguration(proxyIpAddress: proxyIpAddress, proxyPort: proxyPortUInt16) + } + + public func urlSessionConfiguration(proxyIpAddress: String, proxyPort: UInt16) -> URLSessionConfiguration { + let urlSessionConfig = URLSessionConfiguration.default + urlSessionConfig.connectionProxyDictionary = [ + "HTTPEnable": true, + "HTTPProxy": proxyIpAddress, + "HTTPPort": proxyPort, + "HTTPSEnable": true, + "HTTPSProxy": proxyIpAddress, + "HTTPSPort": proxyPort + ] + + print("Getting configuration, \(urlSessionConfig), \(proxyIpAddress), \(proxyPort)") + + return urlSessionConfig + } + + func updateProxySettings(ipAddress: String, port: UInt16) { + proxyIpAddress = ipAddress + proxyPort = Int(port) + print("⚙️ Changed ProxySettingsProvider to ipAddress:\(ipAddress) and port:\(port)") + + guard let configurableProxySessionProvider = Container.shared.configurableProxySessionProvider.resolve() else { return } + + Task { + let urlConfig = urlSessionConfiguration(proxyIpAddress: proxyIpAddress, proxyPort: proxyPortUInt16) + let newConfiguration = await NetworkSessionConfiguration( + urlSessionConfiguration: urlConfig, + interceptor: configurableProxySessionProvider.currentConfiguration.interceptor, + serverTrustManager: configurableProxySessionProvider.currentConfiguration.serverTrustManager, + eventMonitors: configurableProxySessionProvider.currentConfiguration.eventMonitors + ) + await configurableProxySessionProvider.updateConfiguration(with: newConfiguration) + } + } + + func clean() { + proxyIpAddress = "" + proxyPort = 8080 + } + + func testProxyAt(ipAddress: String, port: UInt16) async -> ProxyConfigurationTestResult { + let urlSessionConfiguration = urlSessionConfiguration(proxyIpAddress: ipAddress, proxyPort: port) + print("🧪 Testing Proxy: \( ipAddress), \(port)") + + let testingSession = URLSession(configuration: urlSessionConfiguration) + + let testingUrl = URL(string: "https://mitm.it")! + + do { + let _ = try await testingSession.data(from: testingUrl) + isProxyValidated = true + return .success + } catch let error as NSError where error.code == 310 || error.code == -1200 { // certificate compromised + return .missingCertificate + } catch _ { + return .connectionFailed + } + } + +} diff --git a/Sources/AppDebugMode/Dependencies/PackageManager.swift b/Sources/AppDebugMode/Dependencies/PackageManager.swift new file mode 100644 index 0000000..73326cf --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/PackageManager.swift @@ -0,0 +1,131 @@ +// +// AppDebugModeProvider.swift +// +// +// Created by Matus Klasovity on 27/06/2023. +// + +import Combine +import SwiftUI +import Factory +import PulseProxy +import Pulse +import GoodNetworking + +public struct ApiServerPickerConfiguration { + + public init( + serversCollections: ApiServerCollection, + onSelectedServerChange: CheckedContinuation? = nil + ) { + self.serversCollections = serversCollections + #warning("TODO: Leaking continuation fix") + self.onSelectedServerChange = onSelectedServerChange + } + + var serversCollections: ApiServerCollection + var onSelectedServerChange: CheckedContinuation? + +} + +public actor PackageManager { + + init() {} + + public static let shared = PackageManager() + + // MARK: - Injected + + @Injected(\.sessionManager) private var sessionManager: DebugManSessionManager + @Injected(\.outputProcessor) private var outputProcessor: StandardOutputProcessor + @Injected(\.profileProvider) private var profileProvider: UserProfilesProvider + @Injected(\.proxySettingsProvider) private var proxyProvider: ProxySettingsProvider + + // MARK: - Internal - Variables + + internal var customControls: any View = EmptyView() + internal var customControlsViewIsVisible: Bool { + !(customControls is EmptyView) + } + + // MARK: - State + + @AppStorage("shouldRedirectLogsToAppDebugMode", store: UserDefaults(suiteName: Constants.suiteName)) + var shouldRedirectLogsToAppDebugMode = !DebuggerService.debuggerConnected() + + // MARK: - Variable + + private var userProfileProvider = UserProfilesProvider() + + // MARK: - Public - Variables + + public var selectedUserProfile: UserProfile? { + userProfileProvider.selectedUserProfile + } + +} + +// MARK: - Public - Helper functions + +public extension PackageManager { + + /// Setup the AppDebugModeProvider with the given parameters. + /// - Parameters: + /// - serversCollections: The collections of servers to be displayed in the app debug mode. + /// - onServerChange: The closure to be called when the server is changed. + /// - cacheManager: The cache manager to be used in the app debug mode. + /// - firebaseMessaging: The Firebase Messaging to be used in the app debug mode. + /// - customControls: The custom controls to be displayed in the app debug mode. + /// + func setup( + serverProviders: [DebugSelectableServerProvider], + configurableProxySessionProvider: ConfigurableSessionProvider?, + firebaseMessaging: AnyObject? = nil, + customControls: (some View)? = nil, + showDebugSwift: Bool = true + ) async { + NetworkLogger.enableProxy() + + if showDebugSwift { + Task {@MainActor in +// DebugSwift.setup() +// DebugSwift.show() + } + } + + if !serverProviders.isEmpty { + Container.shared.setupServerProviders(providers: serverProviders) + } + + if let configurableProxySessionProvider { + Container.shared.setupConfigurableProxySessionProvider(provider: configurableProxySessionProvider) + Task { + await proxyProvider.updateProxySettings(ipAddress: proxyProvider.proxyIpAddress, port: proxyProvider.proxyPortUInt16) + } + } + + if let firebaseMessaging { + Container.shared.setupAPNSProvider(firebaseMessaging: firebaseMessaging as! AppDebugFirebaseMessaging) + } + + if shouldRedirectLogsToAppDebugMode { + await outputProcessor.redirectLogsToAppDebugMode() + } + + self.customControls = customControls + } + + @MainActor + func start() async -> UIViewController { + let viewController = await AppDebugView( + customControls: AnyView(customControls), + customControlsViewIsVisible: customControlsViewIsVisible + ) + .eraseToUIViewController() + + let navigationController = UINavigationController(rootViewController: viewController) + navigationController.navigationBar.configureSolidAppearance() + return navigationController + } + +} diff --git a/Sources/AppDebugMode/Dependencies/UserProfilesProvider.swift b/Sources/AppDebugMode/Dependencies/UserProfilesProvider.swift new file mode 100644 index 0000000..33e0383 --- /dev/null +++ b/Sources/AppDebugMode/Dependencies/UserProfilesProvider.swift @@ -0,0 +1,20 @@ +// +// UserProfilesProvider.swift +// AppDebugMode-iOS +// +// Created by Matus Klasovity on 26/06/2023. +// + +import SwiftUI + +public struct UserProfilesProvider { + + // MARK: - Cache + + @AppStorage("testingUsers", store: UserDefaults(suiteName: Constants.suiteName)) + private(set) var userProfiles: [UserProfile] = [] + + @AppStorage("selectedUserUser", store: UserDefaults(suiteName: Constants.suiteName)) + private(set) var selectedUserProfile: UserProfile? + +} diff --git a/Sources/AppDebugMode/DependencyContainer.swift b/Sources/AppDebugMode/DependencyContainer.swift new file mode 100644 index 0000000..8eb9fdf --- /dev/null +++ b/Sources/AppDebugMode/DependencyContainer.swift @@ -0,0 +1,85 @@ +// +// DependencyContainer.swift +// AppDebugMode +// +// Created by Andrej Jasso on 18/09/2024. +// + +import Factory +import GoodNetworking + +public extension Container { + + var packageManager: Factory { + Factory(self) { PackageManager.shared }.singleton + } + + var outputProcessor: Factory { + Factory(self) { StandardOutputProcessor.shared }.singleton + } + + var profileProvider: Factory { + Factory(self) { UserProfilesProvider() }.singleton + } + + + var apnsProviding: Factory { + Factory(self) { nil } + } + + func setupAPNSProvider(firebaseMessaging: AnyObject) { + if let provider = PushNotificationsProvider(firebaseMessaging: firebaseMessaging) { + Container.shared.apnsProviding.register { provider } + } + } + + var debugServerSelectors: Factory<[DebugSelectableServerProvider]> { + Factory(self) { [] }.singleton + } + + func setupServerProviders(providers: [DebugSelectableServerProvider]) { + Container.shared.debugServerSelectors.register { + providers + } + } + + var configurableProxySessionProvider: Factory { + Factory(self) { nil }.singleton + } + + func setupConfigurableProxySessionProvider(provider: ConfigurableSessionProvider) { + Container.shared.configurableProxySessionProvider.register { + provider + } + } + + +} + +// MARK: - MC Session for DebugMan + +extension Container { + + var nearbyServicesBrowserDelegate: Factory { + Factory(self) { PeerBrowserDelegateWrapper() }.singleton + } + + var sessionDelegateWrapper: Factory { + Factory(self) { PeerSessionDelegateWrapper() }.singleton + } + + var sessionManager: Factory { + Factory(self) { + let sessionManager = DebugManSessionManager() + Task { + await sessionManager.start() + } + return sessionManager + }.singleton + } + + var proxySettingsProvider: Factory { + Factory(self) { ProxySettingsProvider() }.singleton + } + +} diff --git a/Sources/AppDebugMode/Extensions/Foundation/NSPredicateExtension.swift b/Sources/AppDebugMode/Extensions/Foundation/NSPredicateExtension.swift deleted file mode 100644 index fadcf40..0000000 --- a/Sources/AppDebugMode/Extensions/Foundation/NSPredicateExtension.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// NSPredicateExtension.swift -// -// -// Created by Matus Klasovity on 27/06/2023. -// - -import Foundation - -extension NSPredicate { - - static let url = NSPredicate(format: "SELF MATCHES %@", "^https://[A-Za-z0-9.-]{2,}\\.[A-Za-z]{2,}(?:/[^\\s]*)?$") - -} diff --git a/Sources/AppDebugMode/Extensions/SwiftUI/ColorExtension.swift b/Sources/AppDebugMode/Extensions/SwiftUI/ColorExtension.swift deleted file mode 100644 index bba93be..0000000 --- a/Sources/AppDebugMode/Extensions/SwiftUI/ColorExtension.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ColorExtension.swift -// -// -// Created by Matus Klasovity on 19/09/2023. -// - -import SwiftUI - -extension Color { - - init(hex: String) { - let scanner = Scanner(string: hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)) - var hexNumber: UInt64 = 0 - - if scanner.scanHexInt64(&hexNumber) { - let red = Double((hexNumber & 0xFF0000) >> 16) / 255.0 - let green = Double((hexNumber & 0x00FF00) >> 8) / 255.0 - let blue = Double(hexNumber & 0x0000FF) / 255.0 - - self.init(red: red, green: green, blue: blue) - return - } - - self.init(red: 0, green: 0, blue: 0) - } - -} - diff --git a/Sources/AppDebugMode/Extensions/UIKit/UIViewControllerExtension.swift b/Sources/AppDebugMode/Extensions/UIKit/UIViewControllerExtension.swift index f95ae00..9ce606c 100644 --- a/Sources/AppDebugMode/Extensions/UIKit/UIViewControllerExtension.swift +++ b/Sources/AppDebugMode/Extensions/UIKit/UIViewControllerExtension.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Factory // MARK: - View Controller Extension @@ -13,13 +14,14 @@ extension UIViewController { open override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) { if motion == .motionShake { - let viewController = AppDebugModeProvider.shared.start() - - if presentedViewController == nil, view.window?.rootViewController?.presentedViewController == nil { - viewController.modalPresentationStyle = .fullScreen - present(viewController, animated: true) - } else { - dismiss(animated: true) + Task { + let viewController = await Container.shared.packageManager.resolve().start() + if presentedViewController == nil, view.window?.rootViewController?.presentedViewController == nil { + viewController.modalPresentationStyle = .fullScreen + present(viewController, animated: true) + } else { + dismiss(animated: true) + } } } } diff --git a/Sources/AppDebugMode/Models/FirebaseMessaging.swift b/Sources/AppDebugMode/Models/APNS/FirebaseMessaging.swift similarity index 100% rename from Sources/AppDebugMode/Models/FirebaseMessaging.swift rename to Sources/AppDebugMode/Models/APNS/FirebaseMessaging.swift diff --git a/Sources/AppDebugMode/Models/APNS/PushNotificationsProvider.swift b/Sources/AppDebugMode/Models/APNS/PushNotificationsProvider.swift new file mode 100644 index 0000000..ab70a32 --- /dev/null +++ b/Sources/AppDebugMode/Models/APNS/PushNotificationsProvider.swift @@ -0,0 +1,28 @@ +// +// File.swift +// +// +// Created by Matus Klasovity on 23/08/2023. +// + +import Foundation + +public final class PushNotificationsProvider { + + var token: String? + let deleteToken: () async throws -> Void + let getToken: () async throws -> String + + init?(firebaseMessaging: AnyObject) { + let type: AnyObject.Type = type(of: firebaseMessaging) + class_addProtocol(type, AppDebugFirebaseMessaging.self) + + if let appDebugFirebaseMesaging = firebaseMessaging as? AppDebugFirebaseMessaging { + self.token = appDebugFirebaseMesaging.fmcToken + self.deleteToken = appDebugFirebaseMesaging.deleteToken + self.getToken = appDebugFirebaseMesaging.token + } + return nil + } + +} diff --git a/Sources/AppDebugMode/Models/ApiServerCollection.swift b/Sources/AppDebugMode/Models/ApiServerCollection.swift deleted file mode 100644 index 27246a7..0000000 --- a/Sources/AppDebugMode/Models/ApiServerCollection.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// ApiServerCollection.swift -// -// -// Created by Matus Klasovity on 02/07/2023. -// - -import Foundation - -public final class ApiServerCollection: ObservableObject { - - // MARK: - Properties - - let name: String - let note: String - let servers: [ApiServer] - - // MARK: - Published State - - @Published var selectedServer: ApiServer - @Published var textContent = "" - @Published var hasValidationError = false - - // MARK: - Init - - public init(name: String, note: String = "", servers: [ApiServer], defaultSelectedServer: ApiServer) { - self.name = name - self.note = note - self.servers = servers - - if let selectedServer = try? UserDefaults.standard.getObject(forKey: name, castTo: ApiServer.self) { - self.selectedServer = selectedServer - } else { - self.selectedServer = defaultSelectedServer - } - } - - - // MARK: - Methods - - func useCustomURL() { - if NSPredicate.url.evaluate(with: textContent) { - selectedServer = ApiServer(name: "Custom", url: textContent) - } else { - hasValidationError = true - } - } - - func saveSelectedServer() { - try! UserDefaults.standard.setObject(selectedServer, forKey: name) - AppDebugModeProvider.shared.onServerChange?() - } - -} - -// MARK: - Equatable - -extension ApiServerCollection: Equatable { - - public static func == (lhs: ApiServerCollection, rhs: ApiServerCollection) -> Bool { - lhs.name == rhs.name && lhs.servers == rhs.servers && lhs.selectedServer == rhs.selectedServer - } - -} diff --git a/Sources/AppDebugMode/Models/ApiServer.swift b/Sources/AppDebugMode/Models/ApiServerSelection/ApiServer.swift similarity index 76% rename from Sources/AppDebugMode/Models/ApiServer.swift rename to Sources/AppDebugMode/Models/ApiServerSelection/ApiServer.swift index 29d8023..f32f657 100644 --- a/Sources/AppDebugMode/Models/ApiServer.swift +++ b/Sources/AppDebugMode/Models/ApiServerSelection/ApiServer.swift @@ -1,14 +1,12 @@ // // ApiServer.swift // -// // Created by Matus Klasovity on 27/06/2023. // import Foundation -import GoodPersistence -public struct ApiServer: Codable, Equatable, Hashable { +public struct ApiServer: Codable, Equatable, Hashable, Sendable { public let name: String public let url: String diff --git a/Sources/AppDebugMode/Models/ApiServerSelection/ApiServerCollection.swift b/Sources/AppDebugMode/Models/ApiServerSelection/ApiServerCollection.swift new file mode 100644 index 0000000..d0e344d --- /dev/null +++ b/Sources/AppDebugMode/Models/ApiServerSelection/ApiServerCollection.swift @@ -0,0 +1,21 @@ +// +// ApiServerCollection.swift +// +// Created by Matus Klasovity on 02/07/2023. +// + +import Foundation + +public struct ApiServerCollection: Sendable, Hashable { + + let name: String + let servers: [ApiServer] + let defaultServer: ApiServer + + public init(name: String, servers: [ApiServer], defaultServer: ApiServer) { + self.name = name + self.servers = servers + self.defaultServer = defaultServer + } + +} diff --git a/Sources/AppDebugMode/Models/Logging/Log.swift b/Sources/AppDebugMode/Models/Logging/Log.swift new file mode 100644 index 0000000..226acab --- /dev/null +++ b/Sources/AppDebugMode/Models/Logging/Log.swift @@ -0,0 +1,94 @@ +// +// Log.swift +// AppDebugMode +// +// Created by Andrej Jasso on 18/09/2024. +// + +import Foundation + +// MARK: - Log + +struct Log: Identifiable, Hashable, Sendable, Codable, RawRepresentable { + + var rawValue: String { + "\(message) \(date) \(id)" + } + + var id: UInt64 { + absoluteSystemTime + } + + let message: String + let date: Date + + private let absoluteSystemTime: UInt64 + + init?(rawValue: String) { + let components = rawValue.split(separator: "|") + guard components.count == 3 else { return nil } + + // Extract message + self.message = String(components[0]) + + // Extract date + let dateString = String(components[1]) + guard let parsedDate = ISO8601DateFormatter().date(from: dateString) else { return nil } + self.date = parsedDate + + // Extract absoluteSystemTime (id) + guard let idValue = UInt64(components[2]) else { return nil } + self.absoluteSystemTime = idValue + } + + init(message: String) { + self.message = message + self.date = Date() + self.absoluteSystemTime = mach_absolute_time() + } + + // Custom Coding Keys + enum CodingKeys: String, CodingKey { + case message + case date + case absoluteSystemTime + } + + // Implement Encodable + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(message, forKey: .message) + try container.encode(date, forKey: .date) + try container.encode(absoluteSystemTime, forKey: .absoluteSystemTime) + } + + // Implement Decodable + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + message = try container.decode(String.self, forKey: .message) + date = try container.decode(Date.self, forKey: .date) + absoluteSystemTime = try container.decode(UInt64.self, forKey: .absoluteSystemTime) + } + +} + +extension Array: @retroactive RawRepresentable where Element: Codable { + public init?(rawValue: String) { + #warning("When adding new element don't decode and reencode all the list but just append data") + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode([Element].self, from: data) + else { + return nil + } + self = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/DebugManConnectionState.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/DebugManConnectionState.swift new file mode 100644 index 0000000..6a6b1f3 --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/DebugManConnectionState.swift @@ -0,0 +1,184 @@ +// +// DebugManConnectionState.swift +// AppDebugMode +// +// Created by Andrej Jasso on 04/10/2024. +// + +import Foundation +import Foundation + +import Foundation + +enum DebugManConnectionState: RawRepresentable, Sendable, Codable { + + case clean + case browsing + case waitingForAccept(from: Peer) + case connecting(to: Peer) + case connected(to: Peer) + case disconnected(from: Peer) + + typealias RawValue = String + + // Raw value conversion + var rawValue: String { + switch self { + case .clean: + return "clean" + case .browsing: + return "browsing" + case .waitingForAccept(let peer): + return "waitingForAccept:\(peer.rawValue)" + case .connecting(let peer): + return "connecting:\(peer.rawValue)" + case .connected(let peer): + return "connected:\(peer.rawValue)" + case .disconnected(let peer): + return "disconnected:\(peer.rawValue)" + } + } + + // Initialize from raw value + init?(rawValue: String) { + let components = rawValue.split(separator: ":").map { String($0) } + guard let firstComponent = components.first else { return nil } + + switch firstComponent { + case "clean": + self = .clean + case "browsing": + self = .browsing + case "waitingForAccept": + guard let peerRawValue = components.dropFirst().first, + let peer = Peer(rawValue: peerRawValue) else { return nil } + self = .waitingForAccept(from: peer) + case "connecting": + guard let peerRawValue = components.dropFirst().first, + let peer = Peer(rawValue: peerRawValue) else { return nil } + self = .connecting(to: peer) + case "connected": + guard let peerRawValue = components.dropFirst().first, + let peer = Peer(rawValue: peerRawValue) else { return nil } + self = .connected(to: peer) + case "disconnected": + guard let peerRawValue = components.dropFirst().first, + let peer = Peer(rawValue: peerRawValue) else { return nil } + self = .disconnected(from: peer) + default: + return nil + } + } + + // MARK: - Codable + + enum CodingKeys: String, CodingKey { + case type + case peer + } + + // Encode the enum into a JSON-compatible format + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .clean: + try container.encode("clean", forKey: .type) + case .browsing: + try container.encode("browsing", forKey: .type) + case .waitingForAccept(let peer): + try container.encode("waitingForAccept", forKey: .type) + try container.encode(peer, forKey: .peer) + case .connecting(let peer): + try container.encode("connecting", forKey: .type) + try container.encode(peer, forKey: .peer) + case .connected(let peer): + try container.encode("connected", forKey: .type) + try container.encode(peer, forKey: .peer) + case .disconnected(let peer): + try container.encode("disconnected", forKey: .type) + try container.encode(peer, forKey: .peer) + } + } + + // Decode the enum from a JSON-compatible format + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + do { + switch type { + case "clean": + self = .clean + case "browsing": + self = .browsing + case "waitingForAccept": + let peer = try container.decode(Peer.self, forKey: .peer) + self = .waitingForAccept(from: peer) + case "connecting": + let peer = try container.decode(Peer.self, forKey: .peer) + self = .connecting(to: peer) + case "connected": + let peer = try container.decode(Peer.self, forKey: .peer) + self = .connected(to: peer) + case "disconnected": + let peer = try container.decode(Peer.self, forKey: .peer) + self = .disconnected(from: peer) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown DebugManConnectionState type") + } + } catch { + print(error.localizedDescription) + throw error + } + } +} + +extension DebugManConnectionState { + + var title: String { + switch self { + case .clean: + return "clean" + case .browsing: + return "browsing" + case .waitingForAccept: + return "waitingForAccept" + case .connecting: + return "connecting" + case .connected: + return "connected" + case .disconnected: + return "disconnected" + } + } + + var isWaitingForAccept: Bool { + switch self { + case .waitingForAccept: return true + default: return false + } + } + + var connecting: Bool { + switch self { + case .connecting: return true + default: return false + } + } + + var isConnected: Bool { + switch self { + case .connected: return true + default: return false + } + } + + var isDisconnected: Bool { + switch self { + case .disconnected: return true + default: return false + } + } + +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/DebugmanSessionState.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/DebugmanSessionState.swift new file mode 100644 index 0000000..8f7e968 --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/DebugmanSessionState.swift @@ -0,0 +1,144 @@ +// +// SessionState.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +enum DebugmanSessionState: Equatable, Sendable, RawRepresentable, Codable { + + case error(description: String) + case notPaired + case pairingBonjour + case requestingCertificate + case requireCertificateTrust + case pairedDisconnected + case pairedConnecting + case pairedConnected + + // RawRepresentable conformance using String + init?(rawValue: String) { + if rawValue.hasPrefix("error:") { + let errorMessage = String(rawValue.dropFirst("error:".count)) + self = .error(description: errorMessage) + } else { + switch rawValue { + case "notPaired": + self = .notPaired + case "pairingBonjour": + self = .pairingBonjour + case "requestingCertificate": + self = .requestingCertificate + case "requireCertificateTrust": + self = .requireCertificateTrust + case "pairedDisconnected": + self = .pairedDisconnected + case "pairedConnecting": + self = .pairedConnecting + case "pairedConnected": + self = .pairedConnected + default: + return nil + } + } + } + + var rawValue: String { + switch self { + case .error(let description): + return "error:\(description)" + case .notPaired: + return "notPaired" + case .pairingBonjour: + return "pairingBonjour" + case .requestingCertificate: + return "requestingCertificate" + case .requireCertificateTrust: + return "requireCertificateTrust" + case .pairedDisconnected: + return "pairedDisconnected" + case .pairedConnecting: + return "pairedConnecting" + case .pairedConnected: + return "pairedConnected" + } + } + + // Codable conformance to handle encoding/decoding + enum CodingKeys: String, CodingKey { + case type + case description + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + switch type { + case "error": + let description = try container.decode(String.self, forKey: .description) + self = .error(description: description) + case "notPaired": + self = .notPaired + case "pairingBonjour": + self = .pairingBonjour + case "requestingCertificate": + self = .requestingCertificate + case "requireCertificateTrust": + self = .requireCertificateTrust + case "pairedDisconnected": + self = .pairedDisconnected + case "pairedConnecting": + self = .pairedConnecting + case "pairedConnected": + self = .pairedConnected + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown session state") + } + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .error(let description): + try container.encode("error", forKey: .type) + try container.encode(description, forKey: .description) + case .notPaired: + try container.encode("notPaired", forKey: .type) + case .pairingBonjour: + try container.encode("pairingBonjour", forKey: .type) + case .requestingCertificate: + try container.encode("requestingCertificate", forKey: .type) + case .requireCertificateTrust: + try container.encode("requireCertificateTrust", forKey: .type) + case .pairedDisconnected: + try container.encode("pairedDisconnected", forKey: .type) + case .pairedConnecting: + try container.encode("pairedConnecting", forKey: .type) + case .pairedConnected: + try container.encode("pairedConnected", forKey: .type) + } + } + + var description: String { + switch self { + case .notPaired: + return "No device paired" + case .pairingBonjour: + return "Pairing" + case .requestingCertificate: + return "Configuring proxy" + case .requireCertificateTrust: + return "Configuring certificate" + case .error(_): + return "Error" + case .pairedDisconnected: + return "Disconnected" + case .pairedConnecting: + return "Connecting" + case .pairedConnected: + return "Connected" + } + } +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/JSONPacket.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/JSONPacket.swift new file mode 100644 index 0000000..fff0b3c --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/JSONPacket.swift @@ -0,0 +1,81 @@ +// +// JSONPacket.swift +// AppDebugMode-iOS +// +// Created by Filip Šašala on 12/06/2024. +// + +import Foundation + +enum JSONPacket: Codable, Equatable, Sendable, RawRepresentable { + + case proxyConfiguration(ProxyConfiguration) + case certificateRequest + case requireCertificateTrust + case pairingSuccessful + case requestProxyConfiguration + + init?(rawValue: String) { + let decoder = JSONDecoder() + + if let data = rawValue.data(using: .utf8) { + if let packet = try? decoder.decode(JSONPacket.self, from: data) { + self = packet + return + } + } + return nil + } + + var rawValue: String { + let encoder = JSONEncoder() + if let data = try? encoder.encode(self), let jsonString = String(data: data, encoding: .utf8) { + return jsonString + } + return "{}" + } + + enum CodingKeys: String, CodingKey { + case proxyConfiguration + case certificateRequest + case requireCertificateTrust + case pairingSuccessful + case requestProxyConfiguration + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .proxyConfiguration(let config): + try container.encode(config, forKey: .proxyConfiguration) + case .certificateRequest: + try container.encode(true, forKey: .certificateRequest) + case .requireCertificateTrust: + try container.encode(true, forKey: .requireCertificateTrust) + case .pairingSuccessful: + try container.encode(true, forKey: .pairingSuccessful) + case .requestProxyConfiguration: + try container.encode(true, forKey: .requestProxyConfiguration) + } + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let config = try? container.decode(ProxyConfiguration.self, forKey: .proxyConfiguration) { + self = .proxyConfiguration(config) + } else if (try? container.decode(Bool.self, forKey: .certificateRequest)) != nil { + self = .certificateRequest + } else if (try? container.decode(Bool.self, forKey: .requireCertificateTrust)) != nil { + self = .requireCertificateTrust + } else if (try? container.decode(Bool.self, forKey: .pairingSuccessful)) != nil { + self = .pairingSuccessful + } else if (try? container.decode(Bool.self, forKey: .requestProxyConfiguration)) != nil { + self = .requestProxyConfiguration + } else { + throw DecodingError.dataCorruptedError(forKey: .requestProxyConfiguration, in: container, debugDescription: "Invalid JSONPacket case") + } + } + +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/Peer.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/Peer.swift new file mode 100644 index 0000000..5a9f8ab --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/Peer.swift @@ -0,0 +1,78 @@ + +import Foundation +import MultipeerConnectivity + +struct Peer: RawRepresentable, Identifiable, Hashable, Equatable, Codable, @unchecked Sendable { + + typealias RawValue = String + + static let defaultPeer = Peer(peerId: .init(displayName: "Default")) + + let name: String + let peerId: MCPeerID + + var id: String { name } + + init(peerId: MCPeerID) { + self.name = peerId.displayName + self.peerId = peerId + } + + func asPeerId() -> MCPeerID { + peerId + } + + // MARK: - RawRepresentable + init?(rawValue: RawValue) { + guard let data = rawValue.data(using: .utf8), + let peer = try? JSONDecoder().decode(Peer.self, from: data) else { + return nil + } + self = peer + } + + var rawValue: RawValue { + guard let data = try? JSONEncoder().encode(self), + let jsonString = String(data: data, encoding: .utf8) else { + return "" + } + return jsonString + } + + // MARK: - Codable + private enum CodingKeys: String, CodingKey { + case name + case peerId + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + name = try container.decode(String.self, forKey: .name) + + let peerIdData = try container.decode(Data.self, forKey: .peerId) + guard let unarchivedPeerId = try? NSKeyedUnarchiver.unarchivedObject(ofClass: MCPeerID.self, from: peerIdData) else { + throw DecodingError.dataCorruptedError(forKey: .peerId, in: container, debugDescription: "Unable to decode MCPeerID") + } + peerId = unarchivedPeerId + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + + let archivedData = try NSKeyedArchiver.archivedData(withRootObject: peerId, requiringSecureCoding: true) + try container.encode(archivedData, forKey: .peerId) + } + + // MARK: - Hashable & Equatable + func hash(into hasher: inout Hasher) { + hasher.combine(name) + } + + static func == (lhs: Peer, rhs: Peer) -> Bool { + lhs.id == rhs.id + } + +} + +extension Peer: Comparable { public static func < (lhs: Peer, rhs: Peer) -> Bool { lhs.id < rhs.id } } diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/PeerConnectionState.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/PeerConnectionState.swift new file mode 100644 index 0000000..1915f4f --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/PeerConnectionState.swift @@ -0,0 +1,37 @@ +// +// PeerConnectionState.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +import MultipeerConnectivity + +enum PeerConnectionState: String, Equatable, RawRepresentable { + + case notConnected + case connected + case connecting + + // MARK: - Initializer + + init(mcSessionState: MCSessionState) { + switch mcSessionState { + case .connected: + self = .connected + case .connecting: + self = .connecting + case .notConnected: + self = .notConnected + @unknown default: + self = .notConnected + } + } + +} + +extension PeerConnectionState: Identifiable { + + var id: String { self.rawValue } + +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/PeerSessionError.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/PeerSessionError.swift new file mode 100644 index 0000000..689e9fe --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/PeerSessionError.swift @@ -0,0 +1,84 @@ +// +// PeerSessionError.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +enum PeerSessionError: Error, CustomStringConvertible { + + // Attempting to browse from a wrong state + case browsingBlocked + + // Attempting to stop browsing from a wrong state + case stopBrowsingBlocked + + // Attempting to invite from a wrong state + case invitingBlocked + + // Attempting to reconnect from a wrong state + case reconnectingBlocked + + // Attempting to reconnect from a wrong state + case reconnectingWithoutPeer + + // Attempting to reconnect but saved peer is not available + case reconnectingPeerNotFound + + // Attempting to send logs but connection is not available + case sendingLogsFailed + + // Attempting to send disconnect from a wrong state + case disconnectBlocked + + // Attempting to send unpair from a wrong state + case unpairBlocked + + // Title for each error case, to be used in error alerts + var title: String { + switch self { + case .browsingBlocked: + return "Browsing Blocked" + case .stopBrowsingBlocked: + return "Stop Browsing Blocked" + case .invitingBlocked: + return "Inviting Blocked" + case .reconnectingBlocked: + return "Reconnecting Blocked" + case .reconnectingWithoutPeer: + return "Reconnecting Without Peer" + case .reconnectingPeerNotFound: + return "Reconnecting Peer Not Found" + case .sendingLogsFailed: + return "Sending Logs Failed" + case .disconnectBlocked: + return "Disconnect Blocked" + case .unpairBlocked: + return "Unpair Blocked" + } + } + + // Description for each error case + var description: String { + switch self { + case .browsingBlocked: + return "Browsing is blocked because you can only start browsing from a clean state." + case .stopBrowsingBlocked: + return "Stopping browsing is blocked because you can only stop browsing from a browsing state." + case .invitingBlocked: + return "Inviting is blocked because you can only invite from the browsing state." + case .reconnectingBlocked: + return "Reconnecting is blocked because you can only reconnect from the disconnected state." + case .reconnectingWithoutPeer: + return "Reconnecting is blocked because there is no peer to reconnect with." + case .reconnectingPeerNotFound: + return "Reconnecting failed because the saved peer was not found or is not available." + case .sendingLogsFailed: + return "Failed to send logs because the connection to another peer is not available." + case .disconnectBlocked: + return "Disconnecting is blocked because you can only disconnect when connected to another peer." + case .unpairBlocked: + return "Unpairing is blocked because you can only unpair when paired with another peer." + } + } +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfiguration.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfiguration.swift new file mode 100644 index 0000000..13e62b9 --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfiguration.swift @@ -0,0 +1,13 @@ +// +// ProxyConfiguration.swift +// AppDebugMode-iOS +// +// Created by Filip Šašala on 12/06/2024. +// + +struct ProxyConfiguration: Codable, Equatable, Sendable { + + let ipAddresses: [String] + let port: UInt16 + +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationError.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationError.swift new file mode 100644 index 0000000..306f6c7 --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationError.swift @@ -0,0 +1,90 @@ +// +// ProxyConfigurationError.swift +// AppDebugMode-iOS +// +// Created by Andrej Jasso on 01/10/2024. +// + +enum ProxyConfigurationError: Error, CustomStringConvertible { + + // Attempting to request proxyConfiguration from a wrong state + // Can only request new cetificate from a clean state and while connected + case requestingProxyConfigurationBlocked + + // Attempting to request certificate from a wrong state + // Can only request new cetificate from a clean state and while connected + case requestingNewCertificateBlocked + + // Attempting to request certificate from a wrong state + // Can only request cetificate trust while connected + case requestingCertificateTrustBlocked + + // Attempting to send success from wrong state + // Can only send success from testing state while connected + case sendingSuccessBlocked + + // Attempting to request certificate from a appDebugMode + // Can only request cetificate from a DebugMan + case requestingCertificateFromAppDebugMode + + // Attempting to send packet but connection is not available + // Can only send packets when connected to another peer + case packetSendBlocked + + // Attempting to encode packet had failed + case encodingPacketFailed + + // Testing Certificate failed. Try again when network connected is reestablished + case testingCertificateConnectionFailed + + // Port must be a number + case portIsNotANumber + + // Title for each error case + var title: String { + switch self { + case .requestingNewCertificateBlocked: + return "Requesting New Certificate Blocked" + case .requestingCertificateTrustBlocked: + return "Requesting Certificate Trust Blocked" + case .sendingSuccessBlocked: + return "Sending Success Blocked" + case .requestingCertificateFromAppDebugMode: + return "Requesting Certificate from App Debug Mode" + case .packetSendBlocked: + return "Packet Sending Blocked" + case .encodingPacketFailed: + return "Encoding Packet Failed" + case .testingCertificateConnectionFailed: + return "Testing Certificate Connection Failed" + case .portIsNotANumber: + return "Invalid Port Number" + case .requestingProxyConfigurationBlocked: + return "Requesting proxy configuration blocked" + } + } + + // Description for each error case + var description: String { + switch self { + case .requestingNewCertificateBlocked: + return "You can only request a new certificate from a clean state and while connected." + case .requestingCertificateTrustBlocked: + return "You can only request certificate trust while connected." + case .sendingSuccessBlocked: + return "You can only send success from the testing state while connected." + case .requestingCertificateFromAppDebugMode: + return "You can only request certificates from DebugMan, not App Debug Mode." + case .packetSendBlocked: + return "Packets can only be sent when connected to another peer." + case .encodingPacketFailed: + return "Failed to encode the packet." + case .testingCertificateConnectionFailed: + return "Testing certificate failed. Try again when the network connection is reestablished." + case .portIsNotANumber: + return "The port must be a valid number." + case .requestingProxyConfigurationBlocked: + return "Attempting to request proxyConfiguration from a wrong state. Can only request new cetificate from a clean state and while connected" + } + } +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationState.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationState.swift new file mode 100644 index 0000000..c2a420e --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationState.swift @@ -0,0 +1,163 @@ +// +// ProxyConfigurationState.swift +// AppDebugMode +// +// Created by Andrej Jasso on 04/10/2024. +// + +import Foundation + +enum ProxyConfigurationState: RawRepresentable, Sendable, Codable { + case clean + case waitingForUserToInstalCertificate + case waitingForUserToTrustMITMProxy + case testing(configuration: ProxyConfiguration) + case configured(configuration: ProxyConfiguration) + + typealias RawValue = String + + // Convert the enum to its `RawValue` representation + var rawValue: String { + switch self { + case .clean: + return "clean" + case .waitingForUserToTrustMITMProxy: + return "waitingForPermission" + case .waitingForUserToInstalCertificate: + return "waitingForInstall" + case .testing(let configuration): + return "testing:\(configurationToString(configuration))" + case .configured(let configuration): + return "configured:\(configurationToString(configuration))" + } + } + + var title: String { + switch self { + case .clean: + return "clean" + case .waitingForUserToTrustMITMProxy: + return "waitingForPermission" + case .waitingForUserToInstalCertificate: + return "waitingForInstall" + case .testing: + return "testing" + case .configured: + return "configured" + } + } + + // Initialize the enum from its `RawValue` + init?(rawValue: String) { + let components = rawValue.split(separator: ":").map { String($0) } + guard let firstComponent = components.first else { + return nil + } + + switch firstComponent { + case "clean": + self = .clean + case "waitingForPermission": + self = .waitingForUserToTrustMITMProxy + case "waitingForInstall": + self = .waitingForUserToInstalCertificate + case "testing": + guard let configRawValue = components.dropFirst().first, + let configuration = Self.stringToConfiguration(configRawValue) else { + return nil + } + self = .testing(configuration: configuration) + case "configured": + guard let configRawValue = components.dropFirst().first, + let configuration = Self.stringToConfiguration(configRawValue) else { + return nil + } + self = .configured(configuration: configuration) + default: + return nil + } + } + + // MARK: - Helpers for encoding/decoding ProxyConfiguration to/from String + + private func configurationToString(_ configuration: ProxyConfiguration) -> String { + guard let data = try? JSONEncoder().encode(configuration), + let jsonString = String(data: data, encoding: .utf8) else { + return "" + } + return jsonString + } + + static private func stringToConfiguration(_ rawValue: String) -> ProxyConfiguration? { + guard let data = rawValue.data(using: .utf8), + let configuration = try? JSONDecoder().decode(ProxyConfiguration.self, from: data) else { + return nil + } + return configuration + } + + // MARK: - Codable Conformance + + enum CodingKeys: String, CodingKey { + case type + case configuration + } + + // Encode the enum into a JSON-compatible format + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + + switch self { + case .clean: + try container.encode("clean", forKey: .type) + case .waitingForUserToTrustMITMProxy: + try container.encode("waitingForPermission", forKey: .type) + case .waitingForUserToInstalCertificate: + try container.encode("waitingForInstall", forKey: .type) + case .testing(let configuration): + try container.encode("testing", forKey: .type) + try container.encode(configuration, forKey: .configuration) + case .configured(let configuration): + try container.encode("configured", forKey: .type) + try container.encode(configuration, forKey: .configuration) + } + } + + // Decode the enum from a JSON-compatible format + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let type = try container.decode(String.self, forKey: .type) + + switch type { + case "clean": + self = .clean + case "waitingForPermission": + self = .waitingForUserToTrustMITMProxy + case "waitingForInstall": + self = .waitingForUserToInstalCertificate + case "testing": + let configuration = try container.decode(ProxyConfiguration.self, forKey: .configuration) + self = .testing(configuration: configuration) + case "configured": + let configuration = try container.decode(ProxyConfiguration.self, forKey: .configuration) + self = .configured(configuration: configuration) + default: + throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown MITMProxyState type") + } + } + + var isTesting: Bool { + switch self { + case .testing: return true + default: return false + } + } + + var isConfigured: Bool { + switch self { + case .configured: return true + default: return false + } + } + +} diff --git a/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationTestResult.swift b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationTestResult.swift new file mode 100644 index 0000000..3f27299 --- /dev/null +++ b/Sources/AppDebugMode/Models/MultipeerConnectivity/ProxyConfigurationTestResult.swift @@ -0,0 +1,14 @@ +// +// ProxyConfigurationTestResult.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +enum ProxyConfigurationTestResult { + + case connectionFailed + case missingCertificate + case success + +} diff --git a/Sources/AppDebugMode/Models/PushNotificationsProvider.swift b/Sources/AppDebugMode/Models/PushNotificationsProvider.swift deleted file mode 100644 index b31d89f..0000000 --- a/Sources/AppDebugMode/Models/PushNotificationsProvider.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// File.swift -// -// -// Created by Matus Klasovity on 23/08/2023. -// - -import Foundation - -final class PushNotificationsProvider { - - var token: String? - let deleteToken: () async throws -> Void - let getToken: () async throws -> String - - init(token: String?, deleteToken: @escaping () async throws -> Void, getToken: @escaping () async throws -> String) { - self.token = token - self.deleteToken = deleteToken - self.getToken = getToken - } - -} diff --git a/Sources/AppDebugMode/Models/UserProfile.swift b/Sources/AppDebugMode/Models/UserProfile.swift deleted file mode 100644 index 9eaf669..0000000 --- a/Sources/AppDebugMode/Models/UserProfile.swift +++ /dev/null @@ -1,15 +0,0 @@ -// -// UserProfile.swift -// -// -// Created by Matus Klasovity on 27/06/2023. -// - -import Foundation - -public struct UserProfile: Codable, Equatable { - - public let name: String - public let password: String - -} diff --git a/Sources/AppDebugMode/Models/UserProfileProviding/UserProfile.swift b/Sources/AppDebugMode/Models/UserProfileProviding/UserProfile.swift new file mode 100644 index 0000000..6a348ab --- /dev/null +++ b/Sources/AppDebugMode/Models/UserProfileProviding/UserProfile.swift @@ -0,0 +1,40 @@ +// +// UserProfile.swift +// +// +// Created by Matus Klasovity on 27/06/2023. +// + +import Foundation + +public struct UserProfile: Codable, Equatable, Sendable, RawRepresentable { + + // MARK: - Properties + + public let name: String + public let password: String + + // MARK: - RawRepresentable conformance + + // The raw value will be a JSON string representing the UserProfile + public var rawValue: String { + // Convert the UserProfile to a JSON-encoded string + guard let data = try? JSONEncoder().encode(self) else { return "" } + return String(data: data, encoding: .utf8) ?? "" + } + + // Initializer from rawValue (JSON string) + public init?(rawValue: String) { + // Convert the JSON string back into a UserProfile + guard let data = rawValue.data(using: .utf8), + let profile = try? JSONDecoder().decode(UserProfile.self, from: data) else { return nil } + self = profile + } + + // Initializer for UserProfile + public init(name: String, password: String) { + self.name = name + self.password = password + } + +} diff --git a/Sources/AppDebugMode/Providers/AppDebugModeProvider.swift b/Sources/AppDebugMode/Providers/AppDebugModeProvider.swift deleted file mode 100644 index 9820374..0000000 --- a/Sources/AppDebugMode/Providers/AppDebugModeProvider.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// AppDebugModeProvider.swift -// -// -// Created by Matus Klasovity on 27/06/2023. -// - -import Combine -import SwiftUI - -public final class AppDebugModeProvider { - - // MARK: - Singleton - - public static let shared = AppDebugModeProvider() - - // MARK: - Initialization - - private init() {} - - // MARK: - Internal - Variables - - internal let connectionManager = ConnectionsManager() - internal var servers: [ApiServer] = [] - internal var serversCollections: [ApiServerCollection] = [] - internal var onServerChange: (() -> Void)? - internal var pushNotificationsProvider: PushNotificationsProvider? - internal var customControls: any View = EmptyView() - internal var customControlsViewIsVisible: Bool { - !(customControls is EmptyView) - } - - // MARK: - Public - Variables - - @available(*, deprecated, renamed: "selectedUserProfile") - public var selectedTestingUser: UserProfile? { - UserProfilesProvider.shared.selectedUserProfile - } - - public var selectedUserProfile: UserProfile? { - UserProfilesProvider.shared.selectedUserProfile - } - - public var proxySettingsProvider: ProxySettingsProvider { - ProxySettingsProvider.shared - } - - @available(*, deprecated, renamed: "selectedUserProfilePublisher") - public var selectedTestingUserPublisher = UserProfilesProvider.shared.selectedUserProfilePublisher - public var selectedUserProfilePublisher = UserProfilesProvider.shared.selectedUserProfilePublisher - - public var shouldRedirectLogsToAppDebugMode: Bool { - StandardOutputService.shared.shouldRedirectLogsToAppDebugMode - } - -} - -// MARK: - Public - Helper functions - -public extension AppDebugModeProvider { - - /// Setup the AppDebugModeProvider with the given parameters. - /// - Parameters: - /// - serversCollections: The collections of servers to be displayed in the app debug mode. - /// - onServerChange: The closure to be called when the server is changed. - /// - cacheManager: The cache manager to be used in the app debug mode. - /// - firebaseMessaging: The Firebase Messaging to be used in the app debug mode. - /// - customControls: The custom controls to be displayed in the app debug mode. - func setup( - serversCollections: [ApiServerCollection] = [], - onServerChange: (() -> Void)? = nil, - cacheManager: Any? = nil, - firebaseMessaging: AnyObject? = nil, - @ViewBuilder customControls: () -> some View = { EmptyView() } - ) { - self.serversCollections = serversCollections - self.onServerChange = onServerChange - - if let cacheManager { - CacheProvider.shared.setup(cacheManager: cacheManager) - } - - if let firebaseMessaging { - setupFirebaseMessaging(firebaseMessaging: firebaseMessaging) - } - - if StandardOutputService.shared.shouldRedirectLogsToAppDebugMode { - StandardOutputService.shared.redirectLogsToAppDebugMode() - } - self.customControls = customControls() - } - - func getSelectedServer(for serverCollection: ApiServerCollection) -> ApiServer { - serversCollections.first { $0 == serverCollection }!.selectedServer - } - - func start() -> UIViewController { - let viewController = AppDebugView( - serversCollections: AppDebugModeProvider.shared.serversCollections, - customControls: AnyView( AppDebugModeProvider.shared.customControls), - customControlsViewIsVisible: AppDebugModeProvider.shared.customControlsViewIsVisible - ) - .environmentObject(connectionManager) - .eraseToUIViewController() - - let navigationController = UINavigationController(rootViewController: viewController) - navigationController.navigationBar.configureSolidAppearance() - return navigationController - } - -} - -// MARK: - Private - Helper functions - -private extension AppDebugModeProvider { - - func setupFirebaseMessaging(firebaseMessaging: AnyObject) { - let type: AnyObject.Type = type(of: firebaseMessaging) - class_addProtocol(type, AppDebugFirebaseMessaging.self) - - if let appDebugFirebaseMesaging = firebaseMessaging as? AppDebugFirebaseMessaging { - pushNotificationsProvider = PushNotificationsProvider( - token: appDebugFirebaseMesaging.fmcToken, - deleteToken: appDebugFirebaseMesaging.deleteToken, - getToken: appDebugFirebaseMesaging.token - ) - } - } - -} diff --git a/Sources/AppDebugMode/Providers/CacheProvider.swift b/Sources/AppDebugMode/Providers/CacheProvider.swift deleted file mode 100644 index 6681b7a..0000000 --- a/Sources/AppDebugMode/Providers/CacheProvider.swift +++ /dev/null @@ -1,192 +0,0 @@ -// -// CacheProvider.swift -// -// -// Created by Matus Klasovity on 31/07/2023. -// - -import Foundation -import GoodPersistence -import KeychainAccess - -final class CacheProvider { - - // MARK: - Properties - Private - - private var cacheManager: Any? - - // MARK: - Properties - Public - - @Published var userDefaultValues: [String : String] = [:] - @Published var keychainValues: [String : String] = [:] - - // MARK: - Structs - - struct Wrapper: Codable { - - let value: T - - } - - // MARK: - Shared - - static let shared = CacheProvider() - - // MARK: - Init - - private init() {} - -} - -// MARK: - Public - -extension CacheProvider { - - func setup(cacheManager: Any) { - self.cacheManager = cacheManager - let mirror = Mirror(reflecting: cacheManager) - - mirror.children.forEach { child in - let childMirror = Mirror(reflecting: child.value) - - if String(describing: child.value).contains("UserDefaultValue") { - getUserDefaultValue(childMirror: childMirror) - } else if String(describing: child.value).contains("KeychainValue") { - getKeychainValue(childMirror: childMirror) - } - } - } - - func reload() { - if let cacheManager { - userDefaultValues = [:] - keychainValues = [:] - setup(cacheManager: cacheManager) - } - } - -} - -// MARK: - Private - User Defaults - -private extension CacheProvider { - - func getUserDefaultValue(childMirror: Mirror) { - guard let key = childMirror.children.first(where: { $0.label == "key" })?.value as? String, - let defaultValueChild = childMirror.children.first(where: { $0.label == "defaultValue" }) - else { return } - - let type = type(of: defaultValueChild.value) - - if let decodable = type as? Codable.Type { - userDefaultValues[key] = receiveUserDefaultValueFromPropertyList( - key: key, - valueType: decodable.self, - defaultValue: defaultValueChild.value - ) - } - } - - func receiveUserDefaultValueFromPropertyList(key: String, valueType: T.Type, defaultValue: Any) -> String { - if let data = UserDefaults.standard.value(forKey: key) as? T { - return String(describing: data) - } - - let defaultValueCodable = defaultValue as! Codable - // If the data isn't of the correct type, try to decode it from the Data stored in UserDefaults. - guard let data = UserDefaults.standard.object(forKey: key) as? Data else { return formatValue(value: defaultValueCodable) } - let value = (try? PropertyListDecoder().decode(Wrapper.self, from: data)) // ?.value ?? defaultValue as? T - - return formatValue(value: value) - } - -} - -// MARK: - Public - User Defaults - -extension CacheProvider { - - func clearUserDefaultsValues() { - let domain = Bundle.main.bundleIdentifier! - UserDefaults.standard.removePersistentDomain(forName: domain) - UserDefaults.standard.synchronize() - } - -} - -// MARK: - Private - Keychain Value - -private extension CacheProvider { - - func getKeychainValue(childMirror: Mirror) { - guard let key = childMirror.children.first(where: { $0.label == "key" })?.value as? String, - let defaultValueChild = childMirror.children.first(where: { $0.label == "defaultValue" }) - else { return } - - let type = type(of: defaultValueChild.value) - - if let decodable = type as? Codable.Type { - keychainValues[key] = receiveKeychainValueFromPropertyList( - key: key, - valueType: decodable.self, - defaultValue: defaultValueChild.value - ) - } - } - - func receiveKeychainValueFromPropertyList( - key: String, - valueType: T.Type, - defaultValue: Any - ) -> String { - guard let value = try? Keychain.default.get(key) else { - let defaultValueCodable = defaultValue as! Codable - // Return default value if data cannot be retrieved from Keychain - return formatValue(value: defaultValueCodable) - } - - return formatValue(value: value) - } - -} - -// MARK: - Public - Keychain Value - -extension CacheProvider { - - /// Clear all values from Keychain - /// - Returns: True if all values were cleared successfully, false otherwise - func clearKeychain() -> Bool { - var statuses: [OSStatus] = [] - [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity].forEach { - let status = SecItemDelete([ - kSecClass: $0, - kSecAttrSynchronizable: kSecAttrSynchronizableAny - ] as CFDictionary) - statuses.append(status) - } - - return !statuses.allSatisfy { $0 != errSecSuccess && $0 != errSecItemNotFound } - } - -} - -// MARK: - Private - -private extension CacheProvider { - - func formatValue(value: T) -> String { - do { - let jsonEncoder = JSONEncoder() - - let encodedValue = try jsonEncoder.encode(value) - let json = try JSONSerialization.jsonObject(with: encodedValue, options: []) - let jsonData = try JSONSerialization.data(withJSONObject: json, options: .prettyPrinted) - - return String(decoding: jsonData, as: UTF8.self) - } catch { - return String(describing: value) - } - } - -} diff --git a/Sources/AppDebugMode/Providers/ConnectionsManager.swift b/Sources/AppDebugMode/Providers/ConnectionsManager.swift deleted file mode 100644 index adb519f..0000000 --- a/Sources/AppDebugMode/Providers/ConnectionsManager.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// ConnectionsManager.swift -// -// -// Created by Matus Klasovity on 21/05/2024. -// - -import Foundation -import MultipeerConnectivity - -final class ConnectionsManager: NSObject, ObservableObject { - - let session: MCSession - let browser: MCNearbyServiceBrowser - - @Published var connectedPeers: [MCPeerID] = [] - - override init() { - let appName = Bundle.main.infoDictionary?[kCFBundleNameKey as String] as? String - let peer = MCPeerID( - displayName: UIDevice.current.name + " - " + (appName ?? "") - ) - - browser = MCNearbyServiceBrowser( - peer: peer, - serviceType: "Debugman" - ) - session = MCSession( - peer: peer, - securityIdentity: nil, - encryptionPreference: .none - ) - super.init() - - session.delegate = self - } - - func cancelConnection(to peer: MCPeerID) { - session.cancelConnectPeer(peer) - } - - func sendToAllPeers(message: String) { - guard !session.connectedPeers.isEmpty else { return } - - let data = Data(message.utf8) - do { - try session.send(data, toPeers: session.connectedPeers, with: .reliable) - } catch { - print("error sending data to peers: \(error)") - } - } - -} - - -extension ConnectionsManager: MCSessionDelegate { - - func session(_ session: MCSession, peer peerID: MCPeerID, didChange state: MCSessionState) { - switch state { - case .notConnected: - print("not connected to peer: \(peerID.displayName)") - - case .connecting: - print("connecting to peer: \(peerID.displayName)") - - case .connected: - print("connected to peer: \(peerID.displayName)") - - @unknown default: - print("unknown state: \(state)") - } - - DispatchQueue.main.async { [weak self] in - self?.connectedPeers = session.connectedPeers - } - } - - func session(_ session: MCSession, didReceive data: Data, fromPeer peerID: MCPeerID) { - print("received data from peer: \(peerID)") - } - - func session(_ session: MCSession, didReceive stream: InputStream, withName streamName: String, fromPeer peerID: MCPeerID) { - print("received stream from peer: \(peerID)") - } - - func session(_ session: MCSession, didStartReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, with progress: Progress) { - print("started receiving resource from peer: \(peerID)") - } - - func session(_ session: MCSession, didFinishReceivingResourceWithName resourceName: String, fromPeer peerID: MCPeerID, at localURL: URL?, withError error: (any Error)?) { - print("finished receiving resource from peer: \(peerID)") - } - -} diff --git a/Sources/AppDebugMode/Providers/ProxySettingsProvider.swift b/Sources/AppDebugMode/Providers/ProxySettingsProvider.swift deleted file mode 100644 index 2c48d49..0000000 --- a/Sources/AppDebugMode/Providers/ProxySettingsProvider.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// ProxySettingsProvider.swift -// -// -// Created by Matus Klasovity on 28/05/2024. -// - -import Foundation -import GoodPersistence - -public final class ProxySettingsProvider { - - @UserDefaultValue("proxyIpAdress", defaultValue: "") - var proxyIpAdress: String - - @UserDefaultValue("proxyPort", defaultValue: 8080) - var proxyPort: Int - - public static let shared = ProxySettingsProvider() - - public var urlSessionConfiguration: URLSessionConfiguration { - let urlSessionConfig = URLSessionConfiguration.default - urlSessionConfig.connectionProxyDictionary = [AnyHashable: Any]() - urlSessionConfig.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = 1 - urlSessionConfig.connectionProxyDictionary?[kCFNetworkProxiesHTTPProxy as String] = proxyIpAdress - urlSessionConfig.connectionProxyDictionary?[kCFNetworkProxiesHTTPPort as String] = proxyPort - urlSessionConfig.connectionProxyDictionary?["HTTPSProxy"] = proxyIpAdress - urlSessionConfig.connectionProxyDictionary?["HTTPSPort"] = proxyPort - - return urlSessionConfig - } - - func updateProxySettings(ipAddress: String, port: Int) { - proxyIpAdress = ipAddress - proxyPort = port - - exit(0) - } - -} diff --git a/Sources/AppDebugMode/Providers/UserProfilesProvider.swift b/Sources/AppDebugMode/Providers/UserProfilesProvider.swift deleted file mode 100644 index acd4660..0000000 --- a/Sources/AppDebugMode/Providers/UserProfilesProvider.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// UserProfilesProvider.swift -// -// -// Created by Matus Klasovity on 26/06/2023. -// - -import Combine -import GoodPersistence - -struct UserProfilesProvider { - - // MARK: - Cache - - @UserDefaultValue("testingUsers", defaultValue: []) - private(set) var userProfiles: [UserProfile] - - @UserDefaultValue("selectedTestingUser", defaultValue: nil) - private(set) var selectedUserProfile: UserProfile? - - // MARK: - Shared - - static var shared: Self = .init() - - // MARK: - Methods - - func setUserProfiles(userProfiles: [UserProfile]) { - self.userProfiles = userProfiles - } - - func setSelectedUserProfile(userProfile: UserProfile) { - self.selectedUserProfile = userProfile - } - - // MARK: - Combine - - public lazy var selectedUserProfilePublisher = _selectedUserProfile.publisher.eraseToAnyPublisher() - -} diff --git a/Sources/AppDebugMode/Screens/AppDirectorySettingsView/AppDirectorySettingsViewModel.swift b/Sources/AppDebugMode/Screens/AppDirectorySettingsView/AppDirectorySettingsViewModel.swift index 03c9c72..38befa9 100644 --- a/Sources/AppDebugMode/Screens/AppDirectorySettingsView/AppDirectorySettingsViewModel.swift +++ b/Sources/AppDebugMode/Screens/AppDirectorySettingsView/AppDirectorySettingsViewModel.swift @@ -7,6 +7,7 @@ import UIKit +@MainActor final class AppDirectorySettingsViewModel: ObservableObject { // MARK: - State diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectedPeers/ConnectedPeersView.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectedPeers/ConnectedPeersView.swift deleted file mode 100644 index 7936a17..0000000 --- a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectedPeers/ConnectedPeersView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// ConnectedPeersView.swift -// -// -// Created by Matus Klasovity on 28/05/2024. -// - -import SwiftUI -import MultipeerConnectivity - -struct ConnectedPeersView: View { - - @State var isBrowserPresented: Bool = false - @EnvironmentObject var connectionsManager: ConnectionsManager - - var body: some View { - Group { - if connectionsManager.connectedPeers.isEmpty { - Text("No connected peers") - .foregroundColor(AppDebugColors.textPrimary) - } - - ForEach(connectionsManager.connectedPeers, id: \.displayName) { connectedPeer in - Text(connectedPeer.displayName) - - .foregroundColor(AppDebugColors.textPrimary) - } - .onDelete { offset in - for index in offset { - connectionsManager.cancelConnection(to: connectionsManager.connectedPeers[index]) - } - } - - ButtonFilled(text: "Open browser") { - isBrowserPresented = true - } - .sheet(isPresented: $isBrowserPresented) { - BrowserRepresentableViewController( - browser: connectionsManager.browser, - session: connectionsManager.session - ) - } - } - } - -} - -// MARK: - BrowserRepresentableViewController - -private struct BrowserRepresentableViewController: UIViewControllerRepresentable { - - let browser: MCNearbyServiceBrowser - let session: MCSession - - func makeUIViewController(context: Context) -> UIViewController { - let viewController = MCBrowserViewController( - browser: browser, - session: session - ) - viewController.delegate = context.coordinator - - return viewController - } - - func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator() - } - - final class Coordinator: NSObject, MCBrowserViewControllerDelegate { - - func browserViewControllerDidFinish(_ browserViewController: MCBrowserViewController) { - browserViewController.dismiss(animated: true) - } - - func browserViewControllerWasCancelled(_ browserViewController: MCBrowserViewController) { - browserViewController.dismiss(animated: true) - } - - } - -} diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectionsSettingsView.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectionsSettingsView.swift index 9149b2c..6571b15 100644 --- a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectionsSettingsView.swift +++ b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ConnectionsSettingsView.swift @@ -6,45 +6,258 @@ // import SwiftUI +import Factory struct ConnectionsSettingsView: View { + // MARK: - Factory + + @InjectedObject(\.sessionManager) private var sessionManager: DebugManSessionManager + + // MARK: - State + + @State var error: (any Error)? + + // MARK: - Body + var body: some View { - List { - connectedPeersSection() - proxySettingsSection() + ScrollView { + VStack { + statusView + + Divider() + + switch sessionManager.debugManConnectionState { + case .clean: + pairButton + .padding(.vertical, 24) + + case .browsing: + stopBrowsingButton + NearbyServicesView(error: $error) + + case .waitingForAccept(let peer): + waitingForReplyView(remotePeer: peer) + .padding(.vertical, 24) + + case .connecting(let peer): + connectingView(remotePeer: peer) + .padding(.vertical, 24) + stopBrowsingButton + + case .connected(let peer): + connectedView(remotePeer: peer) + .padding(.vertical, 24) + + case .disconnected(let peer): + disconnectedView(remotePeer: peer) + .padding(.vertical, 24) + } + + Divider() + + switch sessionManager.mitmProxyState { + case .clean: + proxyCleanView + .padding(.vertical, 24) + + case .waitingForUserToInstalCertificate: + RequestingCertificateView(error: $error) + #warning("Handle discating the configuration") + + case .waitingForUserToTrustMITMProxy: + RequireCertificateTrustView(error: $error) + #warning("Handle discating the configuration") + + case .testing: + RequireCertificateTrustView(error: $error) + + case .configured(let configuration): + proxyConfiguredView(configuration: configuration) + .padding(.vertical, 24) + } + + Divider() + + } + .padding() } + .background(AppDebugColors.backgroundPrimary.ignoresSafeArea()) .navigationTitle("Connections") - .listStyle(.insetGrouped) - .listBackgroundColor(AppDebugColors.backgroundPrimary, for: .insetGrouped) + .alert("Error", isPresented: .constant(error != nil), presenting: error) { error in + Button("OK", role: .cancel) { + self.error = nil + } + } message: { error in + if let error = error as? PeerSessionError { + print("Error: \(error.description)") + return Text(error.description) + } else if let error = error as? ProxyConfigurationError { + print("Error: \(error.description)") + return Text(error.description) + } else { + print("Error: \(error.localizedDescription)") + return Text(error.localizedDescription) + } + } + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button(action: { + Task { + sessionManager.cleanState() + } + }, label: { + Text("❌ Session Cache") + }) + } + } } } -// MARK: - Components +// MARK: - Componenets private extension ConnectionsSettingsView { - func connectedPeersSection() -> some View { - Section { - ConnectedPeersView() - .listRowSeparatorColor(AppDebugColors.primary, for: .insetGrouped) - .listRowBackground(AppDebugColors.backgroundSecondary) - } header: { - Text("Connected peers") - .foregroundColor(AppDebugColors.textSecondary) + var statusView: some View { + VStack { + HStack { + Text("AppDebugMode Status") + .bold() + .foregroundStyle(AppDebugColors.textPrimary) + + Spacer() + + Text(sessionManager.debugManConnectionState.title) + .foregroundStyle(AppDebugColors.textSecondary) + } + + HStack { + Text("Proxy Config Status") + .bold() + .foregroundStyle(AppDebugColors.textPrimary) + + Spacer() + + Text(sessionManager.mitmProxyState.title) + .foregroundStyle(AppDebugColors.textSecondary) + } + + HStack { + Text("Local device") + .bold() + .foregroundStyle(AppDebugColors.textPrimary) + + Spacer() + + Text(sessionManager.localPeer.asPeerId().displayName) + .foregroundStyle(AppDebugColors.textSecondary) + } + + HStack { + Text("Remote device") + .bold() + .foregroundStyle(AppDebugColors.textPrimary) + + Spacer() + + Text(sessionManager.remotePeer?.asPeerId().displayName ?? "none") + .foregroundStyle(AppDebugColors.textSecondary) + } + } + } + + var pairButton: some View { + ButtonFilled(text: "Browse for a new device") { + handleThrowingCall(action: { try sessionManager.startBrowsing() }) + } + } + + var stopBrowsingButton: some View { + ButtonFilled(text: "Stop browing for a new device") { + handleThrowingCall(action: { try sessionManager.stopBrowsing() }) + } + } + + func connectingView(remotePeer: Peer) -> some View { + #warning("Handle cancelling connecting state to clean state") + return ProgressView { + Text("Connecting to \(remotePeer.asPeerId().displayName)") + } + .foregroundStyle(AppDebugColors.textPrimary) + .tint(AppDebugColors.textPrimary) + } + + func disconnectedView(remotePeer: Peer) -> some View { + return Group { + Text("You are disconnected from \(remotePeer.asPeerId().displayName)") + .foregroundStyle(AppDebugColors.textPrimary) + ButtonFilled(text: "Reconnect") { + handleThrowingCall(action: { try await sessionManager.reconnectRemotePeer() }) + } + ButtonFilled(text: "Unpair") { + handleThrowingCall(action: { try sessionManager.unpair() }) + } } } - func proxySettingsSection() -> some View { - Section { - ProxySettingsView() - .listRowBackground(AppDebugColors.backgroundSecondary) - .foregroundColor(AppDebugColors.textPrimary) - } header: { - Text("Proxy settings") - .foregroundColor(AppDebugColors.textSecondary) + func waitingForReplyView(remotePeer: Peer) -> some View { + #warning("Handle cancelling waiting state to clean state") + return Group { + ProgressView { + Text("Waiting for remote peer: \(remotePeer.asPeerId().displayName) to accept the invite") + } + .foregroundStyle(AppDebugColors.textPrimary) + .tint(AppDebugColors.textPrimary) } } + func connectedView(remotePeer: Peer) -> some View { + Group { + Text("😘 Good Job you did it! 🎊 You are connected to \(remotePeer.asPeerId().displayName)") + .foregroundStyle(AppDebugColors.textPrimary) + ButtonFilled(text: "Disconnect") { + handleThrowingCall { try sessionManager.disconnect() } + } + ButtonFilled(text: "Unpair") { + handleThrowingCall { try sessionManager.unpair() } + } + } + } + + var proxyCleanView: some View { + Group { + Text("Proxy not configured") + .foregroundStyle(AppDebugColors.textPrimary) + + ButtonFilled(text: "Request config") { + handleThrowingCall { try await sessionManager.requestNewCertificate() } + } + } + } + + func proxyConfiguredView(configuration: ProxyConfiguration) -> some View { + Group { + Text("😘 Good Job you did it! 🎊 Your proxy is configured with \(configuration.ipAddresses), on port \(configuration.port)") + .foregroundStyle(AppDebugColors.textPrimary) + ButtonFilled(text: "Clean Config") { + handleThrowingCall(action: { try sessionManager.cleanConfig() }) + } + } + } + + func handleThrowingCall(action: @escaping () async throws -> Void) { + Task { + do { + try await action() + } catch { + await handleError(error) + } + } + } + + func handleError(_ error: Error) async { + self.error = error + } + } diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsView.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsView.swift deleted file mode 100644 index 84497cc..0000000 --- a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// ProxySettingsView.swift -// -// -// Created by Matus Klasovity on 28/05/2024. -// - -import SwiftUI -@_spi(Advanced) import SwiftUIIntrospect - -struct ProxySettingsView: View { - - @ObservedObject private var viewModel = ProxySettingsViewModel() - - var body: some View { - Group { - Text(""" - Set up a proxy server to monitor network traffic. - The proxy server will be used only for the app. - - To set up a proxy server, enter the IP address and port of the proxy server. - You can find the IP address and port in the **Debugman** app. You also **need to install certificates** on your device from debugman app and trust them. - - To install certificates, open the Debugman app, go to the **Proxy Settings** -> **Share certificates**. Then use airDrop to send the certificates to your device and install them from the Settings app (similiar workflow to bitrise certificates installation). Then click the **Open Cert Trust Settings** button and enable the certificates. - """ - ) - .foregroundColor(AppDebugColors.textPrimary) - .padding(.bottom, 8) - - TextField("", text: $viewModel.proxyIpAdress) - .introspect(.textField, on: .iOS(.v14...)) { textField in - textField.attributedPlaceholder = NSAttributedString( - string: "Proxy ip address", - attributes: [ - .foregroundColor: UIColor(AppDebugColors.textSecondary) - ] - ) - } - .disableAutocorrection(true) - .autocapitalization(.none) - .keyboardType(.URL) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 8).stroke(AppDebugColors.textSecondary, lineWidth: 1) - ) - .foregroundColor(AppDebugColors.textPrimary) - - TextField("", text: $viewModel.proxyPort) - .introspect(.textField, on: .iOS(.v14...)) { textField in - textField.attributedPlaceholder = NSAttributedString( - string: "Proxy port", - attributes: [ - .foregroundColor: UIColor(AppDebugColors.textSecondary) - ] - ) - } - .disableAutocorrection(true) - .autocapitalization(.none) - .keyboardType(.numberPad) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 8).stroke(AppDebugColors.textSecondary, lineWidth: 1) - ) - .foregroundColor(AppDebugColors.textPrimary) - - ButtonFilled(text: "Open Cert Trust Settings") { - let url = URL(string: "App-prefs:General&path=About/CERT_TRUST_SETTINGS")! - UIApplication.shared.open(url) - } - - ButtonFilled(text: "Save proxy settings") { - viewModel.saveProxySettings() - } - } - .alert(item: $viewModel.validationError) { validationError in - Alert( - title: Text("Validation error"), - message: Text(validationError.localizedDescription), - dismissButton: .cancel() - ) - } - } - -} diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsViewModel.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsViewModel.swift deleted file mode 100644 index 05ead02..0000000 --- a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/ProxySettings/ProxySettingsViewModel.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// ProxySettingsViewModel.swift -// -// -// Created by Matus Klasovity on 28/05/2024. -// - -import Foundation - -enum ProxySettingsValidationError: Error, Identifiable { - - case portIsNotANumber - - var id: String { - self.localizedDescription - } - - var localizedDescription: String { - switch self { - case .portIsNotANumber: - return "Port must be a number" - } - } - -} - -final class ProxySettingsViewModel: ObservableObject { - - @Published var proxyIpAdress: String = ProxySettingsProvider.shared.proxyIpAdress - @Published var proxyPort: String = String(ProxySettingsProvider.shared.proxyPort) - @Published var validationError: ProxySettingsValidationError? - -} - -extension ProxySettingsViewModel { - - func saveProxySettings() { - guard let port = Int(proxyPort) else { - validationError = .portIsNotANumber - - return - } - ProxySettingsProvider.shared.updateProxySettings(ipAddress: proxyIpAdress, port: port) - } - -} diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/NearbyServicesView.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/NearbyServicesView.swift new file mode 100644 index 0000000..6863110 --- /dev/null +++ b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/NearbyServicesView.swift @@ -0,0 +1,180 @@ +// +// NearbyServicesView.swift +// AppDebugMode-iOS +// +// Created by Filip Šašala on 18/06/2024. +// + +import Combine +import SwiftUI +import Factory + +// MARK: - Nearby services view + +struct NearbyServicesView: View { + + // MARK: - Factory + + @Injected(\.sessionManager) private var sessionManager: DebugManSessionManager + @Injected(\.nearbyServicesBrowserDelegate) private var browserDelegateWrapper: PeerBrowserDelegateWrapper + + // MARK: - State + + @State private var peers: Set = [] + @Binding var error: (any Error)? + + // MARK: - Initialization + + init(error: Binding<(any Error)?>) { + self._error = error + } + + // MARK: - Body + + var body: some View { + Group { + if #available(iOS 16, *) { + Wheel { + content + } + } else { + VStack { + content + } + } + } + .task { + Task { + do { + for try await peers in browserDelegateWrapper.peerDiscoveryStream() { + Task { @MainActor in + GRHapticsManager.shared.playCustomPattern(pattern: HapticsPattern.peerDiscovery) + } + self.peers = peers + self.error = nil + } + } catch { + print(error) + self.error = error + } + } + } + } + + @ViewBuilder private var content: some View { + if peers.isEmpty { + HStack { + ProgressView() + + Text("No peers listed in browser") + .foregroundColor(AppDebugColors.textPrimary) + .font(.body) + } + } else { + ForEach(peers.sorted()) { peer in + peerView(peer) + } + } + } + + @ViewBuilder private func peerView(_ peer: Peer) -> some View { + Button { + Task { + try await sessionManager.invite(peer: peer, timeout: 30.0) + } + } label: { + VStack { + Image(systemName: "macbook") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 48, height: 48) + .foregroundColor(AppDebugColors.primary) + + Text(peer.name) + .foregroundColor(AppDebugColors.textPrimary) + .font(.caption2) + } + } + .padding() + .background(AppDebugColors.backgroundSecondary) + .clipShape(.capsule(style: .continuous)) + .shadow(radius: 4) + } + +} + +// MARK: - Wheel layout + +@available(iOS 16, *) +struct Wheel: Layout { + + var rotation: Angle = .degrees(0) + + struct Cache { + var radius: CGFloat + } + + func makeCache(subviews: Subviews) -> Cache { + return Cache(radius: 0) + } + + func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) -> CGSize { + let wheelProposal = proposal.replacingUnspecifiedDimensions(by: .zero) + + let maxSubviewSize = subviews + .map { $0.sizeThatFits(.unspecified) } + .reduce(CGSize.zero) { CGSize(width: max($0.width, $1.width), height: max($0.height, $1.height)) } + + let largestSubviewDimension = max(maxSubviewSize.width, maxSubviewSize.height) + + #warning("TODO: fix with proper calculations based on elements' radii") + var radius = largestSubviewDimension / 2 + for (index, subview) in subviews.dropFirst(2).enumerated() { + let index = index + 2 + radius += largestSubviewDimension / CGFloat(index + subviews.count) + } + + let diameter = 2 * radius + var wheelSize = CGSize( + width: diameter + maxSubviewSize.width, + height: diameter + maxSubviewSize.height + ) + wheelSize.width = max(proposal.width ?? 0, wheelSize.width) + wheelSize.height = max(proposal.height ?? 0, wheelSize.height) + + let maxRadiusHorizontal = proposal.width ?? .infinity - maxSubviewSize.width / 2 + let maxRadiusVertical = proposal.height ?? .infinity - maxSubviewSize.height / 2 + let maxRadius = min(maxRadiusHorizontal, maxRadiusVertical) + + cache.radius = min(radius, maxRadius) + + print(wheelSize) + + return CGSize( + width: proposal.width ?? wheelSize.width, + height: proposal.height ?? wheelSize.height + ) + } + + func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Cache) { + let angleStep = (Angle.degrees(360).radians / Double(subviews.count)) + + for (index, subview) in subviews.enumerated() { + let angle = angleStep * CGFloat(index) + rotation.radians + + var center = CGPoint( + x: bounds.midX, + y: bounds.midY + ) + + var point = CGPoint(x: 0, y: -cache.radius) + .applying(CGAffineTransform(rotationAngle: angle)) + + point.x += bounds.midX + point.y += bounds.midY + + subview.place(at: point, anchor: .center, proposal: .unspecified) + } + } + +} diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequestingCertificateView.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequestingCertificateView.swift new file mode 100644 index 0000000..a9d1204 --- /dev/null +++ b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequestingCertificateView.swift @@ -0,0 +1,92 @@ +// +// RequestingCertificateView.swift +// AppDebugMode-iOS +// +// Created by Filip Šašala on 18/06/2024. +// + +import SwiftUI +import Factory + +// MARK: - Requesting certificate view + +struct RequestingCertificateView: View { + + // MARK: - Factory + + @Injected(\.sessionManager) private var sessionManager: DebugManSessionManager + + // MARK: - Binded + + @Binding var error: (any Error)? + + // MARK: - Initialization + + init(error: Binding<(any Error)?>) { + self._error = error + } + + // MARK: - Body + + var body: some View { + VStack { + switch sessionManager.mitmProxyState { + case .testing: + ProgressView { + Text("Validating certificate...") + } + .foregroundStyle(AppDebugColors.textPrimary) + .tint(AppDebugColors.textPrimary) + + case .waitingForUserToInstalCertificate: + ProgressView { + Text("Continue in Debugman") + } + .foregroundStyle(AppDebugColors.textPrimary) + .tint(AppDebugColors.textPrimary) + + ButtonFilled(text: "Validate Saved Proxy Config") { + Task { + do { + try await sessionManager.validateSavedProxyConfiguration() + } catch { + self.error = error + } + } + } + + ButtonFilled(text: "Request new certificate") { + Task { + do { + try await sessionManager.requestNewCertificate() + } catch { + self.error = error + } + } + } + + ButtonFilled(text: "Request new configuration") { + Task { + do { + try await sessionManager.requestConfiguration() + } catch { + self.error = error + } + } + } + ButtonFilled(text: "Clean Config") { + Task { + do { + try sessionManager.cleanConfig() + } catch { + self.error = error + } + } + } + default: + Text(verbatim: "Something is wront this state should not be possible") + } + } + } + +} diff --git a/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequireCertificateTrustView.swift b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequireCertificateTrustView.swift new file mode 100644 index 0000000..64fb0ff --- /dev/null +++ b/Sources/AppDebugMode/Screens/ConnectionsSettingsView/Views/RequireCertificateTrustView.swift @@ -0,0 +1,87 @@ +// +// RequireCertificateTrustView.swift +// AppDebugMode-iOS +// +// Created by Filip Šašala on 18/06/2024. +// + +import SwiftUI +import Factory + +struct RequireCertificateTrustView: View { + + // MARK: - Factory + + @Injected(\.sessionManager) private var sessionManager: DebugManSessionManager + + // MARK: - Binded + + @Binding var error: (any Error)? + + // MARK: - Initialization + + init(error: Binding<(any Error)?>) { + self._error = error + } + + // MARK: - Body + + var body: some View { + VStack { + switch sessionManager.mitmProxyState { + case .testing: + ProgressView { + Text("Validating certificate...") + } + .foregroundStyle(AppDebugColors.textPrimary) + .tint(AppDebugColors.textPrimary) + + case .waitingForUserToTrustMITMProxy: + ButtonFilled(text: "Open certificate trust settings") { + let url = URL(string: "App-prefs:General&path=About/CERT_TRUST_SETTINGS")! + UIApplication.shared.open(url) + } + + ButtonFilled(text: "Validate Saved Proxy Config") { + Task { + do { + try await sessionManager.validateSavedProxyConfiguration() + } catch { + self.error = error + } + } + } + ButtonFilled(text: "Request new certificate") { + Task { + do { + try await sessionManager.requestNewCertificate() + } catch { + self.error = error + } + } + } + ButtonFilled(text: "Request new config") { + Task { + do { + try await sessionManager.requestNewConfiguration() + } catch { + self.error = error + } + } + } + ButtonFilled(text: "Clean Config") { + Task { + do { + try sessionManager.cleanConfig() + } catch { + self.error = error + } + } + } + default: + Text(verbatim: "Something is wront this state should not be possible") + } + } + } + +} diff --git a/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogDetailView.swift b/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogDetailView.swift index 5614220..b135e36 100644 --- a/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogDetailView.swift +++ b/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogDetailView.swift @@ -7,7 +7,7 @@ import UIKit -class ConsoleLogDetailViewController: UIViewController { +final class ConsoleLogDetailViewController: UIViewController { // MARK: - Views diff --git a/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsView.swift b/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsListView.swift similarity index 82% rename from Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsView.swift rename to Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsListView.swift index 35fec77..1f6123f 100644 --- a/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsView.swift +++ b/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsListView.swift @@ -7,17 +7,31 @@ import SwiftUI -struct ConsoleLogsView: View { +struct ConsoleLogsListView: View { + + // MARK: - Environment @Environment(\.hostingControllerHolder) var viewControllerHolder - @AppStorage("numberOfLinesUnwrapped") var numberOfLinesUnwrapped = 50 - @ObservedObject private var standardOutputService: StandardOutputService - @State var unwrappedIds: Set = [] - @State var isLoading = false - @State var showSettings = false - @State var didScroll = false + // MARK: - State + + @AppStorage("numberOfLinesUnwrapped", store: UserDefaults(suiteName: Constants.suiteName)) + var numberOfLinesUnwrapped = 50 + + @AppStorage("shouldRedirectLogsToAppDebugMode", store: UserDefaults(suiteName: Constants.suiteName)) + var shouldRedirectLogsToAppDebugMode = !DebuggerService.debuggerConnected() + + @AppStorage("capturedOutput", store: UserDefaults(suiteName: Constants.suiteName)) + var capturedOutput: [Log] = [] + // MARK: - State + + @State private var unwrappedIds: Set = [] + @State private var isLoading = false + @State private var showSettings = false + @State private var didScroll = false + + // MARK: - Observed var dateFormatter: DateFormatter { let dateFormatter = DateFormatter() @@ -26,10 +40,6 @@ struct ConsoleLogsView: View { return dateFormatter } - init(standardOutputService: StandardOutputService = StandardOutputService.shared) { - self.standardOutputService = standardOutputService - } - private func toggleLog(with id: UInt64) { if unwrappedIds.contains(id) { unwrappedIds.remove(id) @@ -41,11 +51,11 @@ struct ConsoleLogsView: View { var body: some View { GeometryReader { proxy in ZStack { - if standardOutputService.shouldRedirectLogsToAppDebugMode { - if !standardOutputService.capturedOutput.isEmpty { + if shouldRedirectLogsToAppDebugMode { + if !capturedOutput.isEmpty { consoleLogsList(proxy) } else { - Text("No logs captured yet") + Text("No logs captured yet. capturedOutput:\(capturedOutput), shouldRedirectLogsToAppDebugMode: \(shouldRedirectLogsToAppDebugMode)") .foregroundColor(.white) .bold() .multilineTextAlignment(.center) @@ -74,10 +84,9 @@ struct ConsoleLogsView: View { } .background(AppDebugColors.backgroundPrimary.ignoresSafeArea()) .sheet(isPresented: $showSettings, content: { - ConsoleLogsSettingsView( - standardOutputService: standardOutputService, - showSettings: $showSettings - ) + ConsoleLogsSettingsView( + showSettings: $showSettings + ) }) .navigationTitle("Logs") .toolbar { @@ -90,7 +99,7 @@ struct ConsoleLogsView: View { .disabled(isLoading) Button("Clear") { - standardOutputService.clearLogs() + capturedOutput.removeAll() } .foregroundColor(AppDebugColors.primary) .disabled(isLoading) @@ -100,7 +109,7 @@ struct ConsoleLogsView: View { private func consoleLogsList(_ proxy: GeometryProxy) -> some View { ScrollViewReader { scrollProxy in ZStack { - List(standardOutputService.capturedOutput) { log in + List(capturedOutput) { log in let isUnwrapped = unwrappedIds.contains(log.id) ZStack(alignment: .topTrailing) { VStack(spacing: 0.0) { @@ -149,7 +158,7 @@ struct ConsoleLogsView: View { .listStyle(.plain) .onAppear { if !didScroll { - scrollProxy.scrollTo(standardOutputService.capturedOutput.last?.id, anchor: .top) + scrollProxy.scrollTo(capturedOutput.last?.id, anchor: .top) didScroll = true } isLoading = false @@ -163,7 +172,7 @@ struct ConsoleLogsView: View { .clipShape(Circle()) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) .onTapGesture { - withAnimation {scrollProxy.scrollTo(standardOutputService.capturedOutput.last?.id, anchor: .top) + withAnimation {scrollProxy.scrollTo(capturedOutput.last?.id, anchor: .top) } } } @@ -183,7 +192,7 @@ struct ConsoleLogsView: View { } private func consoleLogMessage( - log: StandardOutputService.Log, + log: Log, proxy: GeometryProxy, isUnwrapped: Bool ) -> some View { @@ -200,8 +209,10 @@ struct ConsoleLogsView: View { } -struct ConsoleLogsView_Previews: PreviewProvider { - static var previews: some View { - ConsoleLogsView(standardOutputService: .testService) - } + + +#Preview { + + ConsoleLogsListView() + } diff --git a/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsSettingsView.swift b/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsSettingsView.swift index 0ea2767..5bf51a7 100644 --- a/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsSettingsView.swift +++ b/Sources/AppDebugMode/Screens/ConsoleLogsVIew/ConsoleLogsSettingsView.swift @@ -1,7 +1,6 @@ // // ConsoleLogsSettingsView.swift.swift // -// // Created by Andrej Jasso on 08/02/2024. // @@ -9,24 +8,33 @@ import SwiftUI struct ConsoleLogsSettingsView: View { - @AppStorage("numberOfLinesUnwrapped") var numberOfLinesUnwrapped = 50 - @ObservedObject var standardOutputService: StandardOutputService - @State private var redirectLogs = false + // MARK: - State + + @AppStorage("numberOfLinesUnwrapped", store: UserDefaults(suiteName: Constants.suiteName)) + var numberOfLinesUnwrapped = 50 + + @AppStorage("shouldRedirectLogsToAppDebugMode", store: UserDefaults(suiteName: Constants.suiteName)) + var shouldRedirectLogsToAppDebugMode = !DebuggerService.debuggerConnected() + + // MARK: - Binding + @Binding var showSettings: Bool - init(standardOutputService: StandardOutputService, showSettings: Binding) { - self.standardOutputService = standardOutputService - self.redirectLogs = standardOutputService.shouldRedirectLogsToAppDebugMode + // MARK: - Initializer + + init(showSettings: Binding) { _showSettings = showSettings UINavigationBar.appearance().titleTextAttributes = [.foregroundColor: UIColor.white] } + // MARK: - Body + var body: some View { NavigationView { ZStack(alignment: .bottom){ List { Group { - Toggle(isOn: $redirectLogs, label: { + Toggle(isOn: $shouldRedirectLogsToAppDebugMode, label: { Text("Redirect Logs To App Debug View") .listRowSeparatorColor(AppDebugColors.primary, for: .insetGrouped) .listRowBackground(AppDebugColors.backgroundSecondary) @@ -38,7 +46,6 @@ struct ConsoleLogsSettingsView: View { ForEach(0..<100) { Text("\($0) lines") .foregroundColor(AppDebugColors.textPrimary) - } } .pickerStyle(MenuPickerStyle()) @@ -58,7 +65,7 @@ struct ConsoleLogsSettingsView: View { ToolbarItem(placement: .topBarTrailing) { Button(action: { withAnimation { - self.redirectLogs = DebuggerService.debuggerConnected() + shouldRedirectLogsToAppDebugMode = DebuggerService.debuggerConnected() } }, label: { Text("Reset") @@ -76,12 +83,6 @@ struct ConsoleLogsSettingsView: View { }) } } - - ButtonFilled(text: "Save Log Settings") { - standardOutputService.shouldRedirectLogsToAppDebugMode = redirectLogs - exit(0) - } - .padding() } .frame(maxHeight: .infinity, alignment: .bottom) } @@ -89,8 +90,8 @@ struct ConsoleLogsSettingsView: View { } -struct ConsoleLogsSettingsView_Previews: PreviewProvider { - static var previews: some View { - ConsoleLogsSettingsView(standardOutputService: .shared, showSettings: .constant(true)) - } +#Preview { + + ConsoleLogsSettingsView(showSettings: .constant(true)) + } diff --git a/Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsView.swift b/Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsView.swift deleted file mode 100644 index 94bd122..0000000 --- a/Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsView.swift +++ /dev/null @@ -1,130 +0,0 @@ -// -// KeychainSettingsView.swift -// -// -// Created by Matus Klasovity on 30/07/2023. -// - -import SwiftUI - -struct KeychainSettingsView: View { - - @ObservedObject private var viewModel = KeychainSettingsViewModel() - @State private var isShowingCopiedAlert = false - - var body: some View { - Group { - if #available(iOS 15, *) { - keychainList() - .safeAreaInset(edge: .bottom) { - reloadKeychainFooter() - } - } else { - ZStack(alignment: .bottom) { - keychainList() - reloadKeychainFooter() - } - } - } - .navigationTitle("Keychain settings") - .copiedAlert(isPresented: $isShowingCopiedAlert) - .alert(item: $viewModel.alert) { alert in - switch alert { - case .clearKeychainError: - return clearKeychainErrorAlert() - - case .confirmation: - return clearKeychainConfirmationAlert() - } - } - } - -} - -// MARK: - Components - -private extension KeychainSettingsView { - - func keychainList() -> some View { - List { - keychainValuesSection() - dangerZoneSection() - } - .listStyle(.insetGrouped) - .listContentInsets(UIEdgeInsets(top: 0, left: 0, bottom: 100, right: 0), for: .insetGrouped) - .listBackgroundColor(AppDebugColors.backgroundPrimary, for: .insetGrouped) - } - - // MARK: - Keychain values - - func keychainValuesSection() -> some View { - Section { - ForEach(viewModel.keychainValues, id: \.key) { key, value in - CacheValueListItemView(key: key, value: value, isShowingCopiedAlert: $isShowingCopiedAlert) - .listRowSeparatorColor(AppDebugColors.primary, for: .insetGrouped) - .listRowBackground(AppDebugColors.backgroundSecondary) - } - } header: { - Text("Keychain values") - .foregroundColor(AppDebugColors.textSecondary) - } - } - - // MARK: - Danger zone - - func dangerZoneSection() -> some View { - Section { - VStack { - ButtonFilled(text: "Clear keychain data", style: .danger) { - viewModel.alert = .confirmation - } - Text("The app will be restarted after clearing the keychain.") - .font(.caption) - .multilineTextAlignment(.leading) - .foregroundColor(Color.gray) - } - .listRowBackground(AppDebugColors.backgroundSecondary) - } header: { - Text("Actions") - .foregroundColor(AppDebugColors.textSecondary) - } - } - - // MARK: - Footer - - func reloadKeychainFooter() -> some View { - VStack(spacing: 10) { - ButtonFilled(text: "Reload keychain data") { - viewModel.reloadKeychain() - } - Text("Reload data from the keychain.") - .font(.caption) - .multilineTextAlignment(.leading) - .foregroundColor(AppDebugColors.textSecondary) - } - .padding(16) - .background(Glass().edgesIgnoringSafeArea(.bottom)) - } - - // MARK: - Alerts - - func clearKeychainConfirmationAlert() -> Alert { - Alert( - title: Text("Do you really want to clear the keychain?"), - message: Text("This action cannot be undone."), - primaryButton: .destructive(Text("Yes, clear keychain")) { - viewModel.clearKeychain() - }, - secondaryButton: .cancel() - ) - } - - func clearKeychainErrorAlert() -> Alert { - Alert( - title: Text("Error"), - message: Text("Keychain data could not be cleared."), - dismissButton: .default(Text("OK")) - ) - } - -} diff --git a/Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsViewModel.swift b/Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsViewModel.swift deleted file mode 100644 index 8f01258..0000000 --- a/Sources/AppDebugMode/Screens/KeychainSettingsView/KeychainSettingsViewModel.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// KeychainSettingsViewModel.swift -// -// -// Created by Matus Klasovity on 30/07/2023. -// - -import Foundation -import Combine - -final class KeychainSettingsViewModel: ObservableObject { - - // MARK: - State - - @Published var alert: Alert? - - @Published var keychainValues = Array(CacheProvider.shared.keychainValues).sorted(by: { $0.key < $1.key }) - - // MARK: - Enums - - enum Alert: Identifiable { - - case clearKeychainError - case confirmation - - var id: String { - UUID().uuidString - } - - } - - // MARK: - Properties - - private var cancellables = Set() - - // MARK: - Init - - init() { - bindState() - } - - // MARK: - Methods - Public - - func clearKeychain() { - let isError = CacheProvider.shared.clearKeychain() - if isError { - alert = .clearKeychainError - } else { - exit(0) - } - } - - func reloadKeychain() { - CacheProvider.shared.reload() - } - -} - -// MARK: - Private - -private extension KeychainSettingsViewModel { - - func bindState() { - CacheProvider.shared.$keychainValues - .map { Array($0).sorted(by: { $0.key < $1.key }) } - .assign(to: \.keychainValues, on: self) - .store(in: &cancellables) - } - -} - diff --git a/Sources/AppDebugMode/Screens/PushNotificationsSettingsView/PushNotificationsSettingsViewModel.swift b/Sources/AppDebugMode/Screens/PushNotificationsSettingsView/PushNotificationsSettingsViewModel.swift index 60e8fb0..2d152e6 100644 --- a/Sources/AppDebugMode/Screens/PushNotificationsSettingsView/PushNotificationsSettingsViewModel.swift +++ b/Sources/AppDebugMode/Screens/PushNotificationsSettingsView/PushNotificationsSettingsViewModel.swift @@ -8,6 +8,7 @@ import SwiftUI import Combine +@MainActor final class PushNotificationsSettingsViewModel: ObservableObject { // MARK: - State @@ -42,7 +43,7 @@ extension PushNotificationsSettingsViewModel { } func refreshToken(shouldRegenerate: Bool) { - Task { @MainActor in + Task { do { if shouldRegenerate { try await pushNotificationsProvider.deleteToken() diff --git a/Sources/AppDebugMode/Screens/ResetAppView/ResetAppView.swift b/Sources/AppDebugMode/Screens/ResetAppView/ResetAppView.swift index b84ef21..4042153 100644 --- a/Sources/AppDebugMode/Screens/ResetAppView/ResetAppView.swift +++ b/Sources/AppDebugMode/Screens/ResetAppView/ResetAppView.swift @@ -6,15 +6,14 @@ // import SwiftUI +import Factory struct ResetAppView: View { - - @ObservedObject private var userDefaultsViewModel = UserDefaultsSettingsViewModel() - @ObservedObject private var keychainViewModel = KeychainSettingsViewModel() + @ObservedObject private var appDirectoryViewModel = AppDirectorySettingsViewModel() @State private var isShowingConfirmationAlert = false - + var body: some View { VStack { ButtonFilled(text: "Remove All App Data", style: .danger) { @@ -40,8 +39,6 @@ private extension ResetAppView { title: Text("Do you really want to erase all app data?"), message: Text("This action cannot be undone."), primaryButton: .destructive(Text("Yes, erase all data")) { - userDefaultsViewModel.clearUserDefaults() - keychainViewModel.clearKeychain() appDirectoryViewModel.clearAppFilesDirectory() exit(0) }, diff --git a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView.swift b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView.swift new file mode 100644 index 0000000..74058f0 --- /dev/null +++ b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView.swift @@ -0,0 +1,227 @@ +// +// ServerPickerView.swift +// +// Created by Matus Klasovity on 26/06/2023. +// + +import SwiftUI +import SwiftUIIntrospect +import Factory + +struct ServerPickerView: View { + + // MARK: - Properties + + private let serverSelector: DebugSelectableServerProvider + + // MARK: - State + + @State private var customURL = "" + @State private var customName = "" + @State private var selectedServer: ApiServer? + @State private var failureAddingServer = false + @State private var serverCollection: ApiServerCollection? + + // MARK: - Initializer + + init(serverSelector: DebugSelectableServerProvider) { + self.serverSelector = serverSelector + } + + // MARK: - Body + + var body: some View { + VStack { + if let serverCollection, let selectedServer { + currentlyPickedServerView( + selectedServer: selectedServer, + serverCollection: serverCollection + ) + } + if let serverCollection { + serverPicker(serverCollection: serverCollection) + } + serverInputView() + } + .alert(isPresented: $failureAddingServer) { + Alert( + title: Text("Validation error"), + message: Text("Please check url format") + ) + } + .onChange(of: selectedServer) { newServer in + hideKeyboard() + Task { + if let newServer { + await serverSelector.setSelectedServer(newServer) + } + } + } + .task { + await updateServerCollection() + selectedServer = await serverSelector.getSelectedServer() + } + } +} +// MARK: - Components + +private extension ServerPickerView { + + func currentlyPickedServerView(selectedServer: ApiServer, serverCollection: ApiServerCollection) -> some View { + VStack(spacing: 4) { + Text(serverCollection.name) + .bold() + .font(.title2) + .foregroundColor(AppDebugColors.primary) + + Text("Currently picked server:") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(AppDebugColors.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom) + + Text(selectedServer.name) + .bold() + .foregroundColor(AppDebugColors.primary) + + Text(selectedServer.url) + .foregroundColor(AppDebugColors.textPrimary) + + } + .font(.subheadline) + .frame(maxWidth: .infinity) + .fixedSize(horizontal: false, vertical: true) + .padding(.vertical, 16) + } + + func serverPicker(serverCollection: ApiServerCollection) -> some View { + VStack { + Text("Select server:") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(AppDebugColors.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom) + + if serverCollection.servers.count > 5 { + Picker("Please select a server", selection: $selectedServer) { + ForEach(serverCollection.servers, id: \.self) { server in + HStack { + Text(server.name) + Text(server.url) + } + .tag(server as ApiServer?) + + } + } + .pickerStyle(.menu) + .tint(AppDebugColors.primary) + .foregroundColor(AppDebugColors.textPrimary) + } else { + Picker("Please select a server", selection: $selectedServer) { + ForEach(serverCollection.servers, id: \.self) { server in + Text(server.name) + .tag(server as ApiServer?) + } + } + .pickerStyle(.segmented) + .introspect(.picker(style: .segmented), on: .iOS(.v14, .v15, .v16, .v17, .v18)) { segmentedControl in + segmentedControl.selectedSegmentTintColor = UIColor(AppDebugColors.primary) + segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.black], for: .selected) + segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor(AppDebugColors.primary)], for: .normal) + } + + } + } + .padding(.vertical, 16) + } + + func serverInputView() -> some View { + VStack { + Text("Need a custom server? Add it here. I will cache it until you uninstall the app") + .font(.subheadline) + .fontWeight(.semibold) + .foregroundColor(AppDebugColors.primary) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.bottom) + + Text("Server Name:") + .foregroundColor(AppDebugColors.textPrimary) + + TextField("Server Name", text: $customName) + .disableAutocorrection(true) + .autocapitalization(.none) + .keyboardType(.URL) + .padding() + .overlay( + RoundedRectangle(cornerRadius: 8).stroke(AppDebugColors.textSecondary, lineWidth: 1) + ) + .foregroundColor(AppDebugColors.textPrimary) + + Text("Server URL:") + .foregroundColor(AppDebugColors.textPrimary) + + TextField("Server URL", text: $customURL) + .disableAutocorrection(true) + .autocapitalization(.none) + .keyboardType(.URL) + .padding() + .overlay( + RoundedRectangle(cornerRadius: 8).stroke(AppDebugColors.textSecondary, lineWidth: 1) + ) + .foregroundColor(AppDebugColors.textPrimary) + + ButtonFilled(text: "Add Custom URL", style: .secondary) { + Task { + do { + try await serverSelector.addCustomServer(customServerUrlString: customURL, customName: customName, collectionName: serverCollection?.name ?? "Default Name") + await updateServerCollection() + } catch { + failureAddingServer = true + } + } + } + + ButtonFilled(text: "Delete Custom Servers", style: .secondary) { + Task { + for customServer in await serverSelector.customServers { + await serverSelector.deleteCustomServer(customServer) + } + await updateServerCollection() + } + } + + Text("NOTE: Add URL in correct fromat, containing domain as well as directory path.") + .font(.caption) + .multilineTextAlignment(.leading) + .foregroundColor(AppDebugColors.textSecondary) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 16) + } + +} + +private extension ServerPickerView { + + func updateServerCollection() async { + let oldServerCollection = await serverSelector.serverCollection + let customServers = await serverSelector.customServers + let newServerCollection = ApiServerCollection.init( + name: oldServerCollection.name, + servers: oldServerCollection.servers + customServers, + defaultServer: oldServerCollection.defaultServer + ) + serverCollection = newServerCollection + } + +} + +#Preview { + + @Injected(\.debugServerSelectors) var debugServerSelectors: [DebugSelectableServerProvider] + + ServerPickerView(serverSelector: debugServerSelectors.first!) + +} diff --git a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerView.swift b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerView.swift deleted file mode 100644 index 37a58e6..0000000 --- a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerView.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// ServerPickerView.swift -// -// -// Created by Matus Klasovity on 26/06/2023. -// - -import SwiftUI -import SwiftUIIntrospect - -struct ServerPickerView: View { - - // MARK: - State - - @ObservedObject var viewModel: ApiServerCollection - - // MARK: - Body - - var body: some View { - VStack { - currentlyPickedServerView(apiServer: viewModel.selectedServer) - serverPicker() - serverInputView() - } - .onChange(of: viewModel.selectedServer) { _ in - hideKeyboard() - viewModel.hasValidationError = false - } - .alert(isPresented: $viewModel.hasValidationError) { - Alert( - title: Text("Validation error"), - message: Text("Please check url format") - ) - } - } - -} - -// MARK: - Components - -private extension ServerPickerView { - - func currentlyPickedServerView(apiServer: ApiServer?) -> some View { - VStack(spacing: 4) { - Text("Currently picked server:") - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(AppDebugColors.primary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom) - - Text(apiServer?.name ?? "No server selected") - .bold() - .foregroundColor(AppDebugColors.primary) - - Text(apiServer?.url ?? "") - .foregroundColor(AppDebugColors.textPrimary) - - } - .font(.subheadline) - .frame(maxWidth: .infinity) - .fixedSize(horizontal: false, vertical: true) - .padding(.vertical, 16) - } - - func serverPicker() -> some View { - VStack { - Text("Select server:") - .font(.subheadline) - .fontWeight(.semibold) - .foregroundColor(AppDebugColors.primary) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.bottom) - - - Picker("Please select a server", selection: $viewModel.selectedServer) { - ForEach(viewModel.servers, id: \.self) { server in - Text(server.name) - .id(server) - } - } - .pickerStyle(.segmented) - .introspect(.picker(style: .segmented), on: .iOS(.v14, .v15, .v16, .v17)) { segmentedControl in - segmentedControl.selectedSegmentTintColor = UIColor(AppDebugColors.primary) - segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor.black], for: .selected) - segmentedControl.setTitleTextAttributes([.foregroundColor: UIColor(AppDebugColors.primary)], for: .normal) - } - } - .padding(.vertical, 16) - } - - func serverInputView() -> some View { - VStack { - TextField("", text: $viewModel.textContent) - .disableAutocorrection(true) - .autocapitalization(.none) - .keyboardType(.URL) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 8).stroke(AppDebugColors.textSecondary, lineWidth: 1) - ) - .foregroundColor(AppDebugColors.textPrimary) - - ButtonFilled(text: "Use Custom URL", style: .secondary) { - viewModel.useCustomURL() - } - - Text("NOTE: Add URL in correct fromat, containing domain as well as directory path.") - .font(.caption) - .multilineTextAlignment(.leading) - .foregroundColor(AppDebugColors.textSecondary) - } - .frame(maxWidth: .infinity) - .padding(.vertical, 16) - } - -} diff --git a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerViewModel.swift b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerViewModel.swift deleted file mode 100644 index 5a9b105..0000000 --- a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServerPickerView/ServerPickerViewModel.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// ServerPickerViewModel.swift -// -// -// Created by Matus Klasovity on 26/06/2023. -// - -import Foundation - -final class ServerPickerViewModel: ObservableObject { - - // MARK: - Properties - - @Published var selectedServer: ApiServer // = ApiServerProvider.shared.apiServer - @Published var textContent = "" - @Published var hasValidationError = false - - // MARK: - Init - - init(selectedServer: ApiServer) { - self.selectedServer = selectedServer - } - - // MARK: - Methods - - func useCustomURL() { - if NSPredicate.url.evaluate(with: textContent) { - selectedServer = ApiServer(name: "Custom", url: textContent) - } else { - hasValidationError = true - } - } - -} diff --git a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionView.swift b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionView.swift index 2befffb..dfb4431 100644 --- a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionView.swift +++ b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionView.swift @@ -1,81 +1,72 @@ // // ServersCollectionView.swift -// // // Created by Matus Klasovity on 02/07/2023. // import SwiftUI +import Factory struct ServersCollectionsView: View { + + // MARK: - Factory + + @Injected(\.debugServerSelectors) private var serverSelectors: [DebugSelectableServerProvider] + + // MARK: - State - // MARK: - Properties - - @ObservedObject var viewModel: ServersCollectionsViewModel - + @State private var serverCollections: [ApiServerCollection] = [] + // MARK: - Body - + var body: some View { Group { - if #available(iOS 15, *) { - serverSettingsList() - .safeAreaInset(edge: .bottom) { - saveServerSettingsFooter() - } - } else { - ZStack(alignment: .bottom) { - serverSettingsList() - saveServerSettingsFooter() - } + serverSettingsList() + } + .navigationTitle("Server collections") + .task { + for await server in serverSelectors.publisher.values { + await serverCollections.append(server.serverCollection) } - }.navigationTitle("Server collections") + } } - + } // MARK: - Components private extension ServersCollectionsView { - - // MARK: - List - + func serverSettingsList() -> some View { List { - ForEach(viewModel.serversCollections, id: \.name) { serverCollection in - serverCollectionSection(serverCollection: serverCollection) + ForEach(serverSelectors, id: \.self) { serverSelector in + serverCollectionSection(serverSelector: serverSelector) } } .listStyle(.insetGrouped) .listContentInsets(UIEdgeInsets(top: 0, left: 0, bottom: 100, right: 0), for: .insetGrouped) .listBackgroundColor(AppDebugColors.backgroundPrimary, for: .insetGrouped) + .safeAreaInset(edge: .bottom) { + footnotes() + } } - + // MARK: - Server Picker List Item View - - func serverCollectionSection(serverCollection: ApiServerCollection) -> some View { + + func serverCollectionSection(serverSelector: DebugSelectableServerProvider) -> some View { Section { VStack(alignment: .leading, spacing: 16) { - title(text: serverCollection.name) - - if !serverCollection.note.isEmpty { - note(text: serverCollection.note) - } - - ServerPickerView(viewModel: serverCollection) + ServerPickerView(serverSelector: serverSelector) } .listRowBackground(AppDebugColors.backgroundSecondary) } } - + // MARK: - Footer - - func saveServerSettingsFooter() -> some View { - VStack(spacing: 10) { - ButtonFilled(text: "Save server settings") { - viewModel.saveServerSettings() - } - Text("App will be terminated in the moment you save the changes due to propper change of picked server.") + func footnotes() -> some View { + VStack(spacing: 10) { + Text("App will be dynamically change the server if you create the debug server selector in the project and retain use the same object to resolved URL's in the request manager. Otherwise you need to restart the app to refresh the server.") .multilineTextAlignment(.center) .foregroundColor(AppDebugColors.textSecondary) .font(.caption) @@ -85,13 +76,6 @@ private extension ServersCollectionsView { .background(Glass().edgesIgnoringSafeArea(.bottom)) } - func title(text: String) -> some View{ - Text(text) - .bold() - .font(.title2) - .foregroundColor(AppDebugColors.primary) - } - func note(text: String) -> some View { Text(text) .font(.caption) @@ -100,13 +84,6 @@ private extension ServersCollectionsView { } -// MARK: - Previews - -struct ServersCollectionsView_Previews: PreviewProvider { - - static var previews: some View { - ServersCollectionsView(viewModel: ServersCollectionsViewModel(serversCollections: [])) - } - +#Preview { + ServersCollectionsView() } - diff --git a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionViewModel.swift b/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionViewModel.swift deleted file mode 100644 index 02f1fad..0000000 --- a/Sources/AppDebugMode/Screens/ServersCollectionsView/ServersCollectionViewModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// ServersCollectionsViewModel.swift -// -// -// Created by Matus Klasovity on 02/07/2023. -// - -import Foundation - -final class ServersCollectionsViewModel: ObservableObject { - - // MARK: - State - - let serversCollections: [ApiServerCollection] - - // MARK: - Init - - init(serversCollections: [ApiServerCollection]) { - self.serversCollections = serversCollections - } - - // MARK: - Methods - - func saveServerSettings() { - serversCollections.forEach { - $0.saveSelectedServer() - } - exit(0) - } - -} diff --git a/Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsView.swift b/Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsView.swift deleted file mode 100644 index c52ca0d..0000000 --- a/Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsView.swift +++ /dev/null @@ -1,117 +0,0 @@ -// -// UserDefaultsSettingsView.swift -// -// -// Created by Matus Klasovity on 30/07/2023. -// - -import SwiftUI - -struct UserDefaultsSettingsView: View { - - @ObservedObject private var viewModel = UserDefaultsSettingsViewModel() - @State private var isShowingConfirmationAlert = false - @State private var isShowingCopiedAlert = false - - var body: some View { - Group { - if #available(iOS 15, *) { - userDefaultsList() - .safeAreaInset(edge: .bottom) { - reloadCacheFooter() - } - } else { - ZStack(alignment: .bottom) { - userDefaultsList() - reloadCacheFooter() - } - } - } - .navigationTitle("User defaults") - .copiedAlert(isPresented: $isShowingCopiedAlert) - .alert(isPresented: $isShowingConfirmationAlert, content: clearUserDefaultsConfirmationAlert) - } - -} - -// MARK: - Components - -private extension UserDefaultsSettingsView { - - func userDefaultsList() -> some View { - List { - cacheValuesSection() - dangerZoneSection() - } - .listStyle(.insetGrouped) - .listContentInsets(UIEdgeInsets(top: 0, left: 0, bottom: 100, right: 0), for: .insetGrouped) - .listBackgroundColor(AppDebugColors.backgroundPrimary, for: .insetGrouped) - } - - // MARK: - Cahce values section - - func cacheValuesSection() -> some View { - Section { - ForEach(viewModel.userDefaultValues, id: \.key) { key, value in - CacheValueListItemView(key: key, value: value, isShowingCopiedAlert: $isShowingCopiedAlert) - .listRowSeparatorColor(AppDebugColors.primary, for: .insetGrouped) - .listRowBackground(AppDebugColors.backgroundSecondary) - } - } header: { - Text("User defaults values") - .foregroundColor(AppDebugColors.textSecondary) - } - } - - // MARK: - Danger zone - - func dangerZoneSection() -> some View { - Section { - VStack(spacing: 16) { - ButtonFilled(text: "Clear user defaults", style: .danger) { - isShowingConfirmationAlert = true - } - Text("The app will be restarted after clearing the user defaults.") - .font(.caption) - .multilineTextAlignment(.center) - .foregroundColor(Color.gray) - } - .listRowBackground(AppDebugColors.backgroundSecondary) - } header: { - Text("Danger zone") - .foregroundColor(AppDebugColors.textSecondary) - } - } - - // MARK: - Footer - - func reloadCacheFooter() -> some View { - VStack(spacing: 10) { - ButtonFilled(text: "Reload user defaults") { - viewModel.reloadUserDefaults() - } - - Text("Reload data from the user defaults.") - .font(.caption) - .multilineTextAlignment(.leading) - .foregroundColor(AppDebugColors.textSecondary) - } - .padding(16) - .background(Glass().edgesIgnoringSafeArea(.bottom)) - } - - // MARK: - Alert - - func clearUserDefaultsConfirmationAlert() -> Alert { - Alert( - title: Text("Do you really want to clear user defaults?"), - message: Text("This action cannot be undone."), - primaryButton: .destructive(Text("Yes, clear user defaults")) { - viewModel.clearUserDefaults() - exit(0) - }, - secondaryButton: .cancel() - ) - } - -} diff --git a/Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsViewModel.swift b/Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsViewModel.swift deleted file mode 100644 index 59a7d3c..0000000 --- a/Sources/AppDebugMode/Screens/UserDefaultsSettingsView/UserDefaultsSettingsViewModel.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// UserDefaultsSettingsViewModel.swift -// -// -// Created by Matus Klasovity on 30/07/2023. -// - -import Foundation -import Combine - -final class UserDefaultsSettingsViewModel: ObservableObject { - - // MARK: - State - - @Published var userDefaultValues = Array(CacheProvider.shared.userDefaultValues).sorted(by: { $0.key < $1.key }) - - // MARK: - Properties - - private var cancellables = Set() - - // MARK: - Init - - init() { - bindState() - } - - // MARK: - Methods - - func clearUserDefaults() { - CacheProvider.shared.clearUserDefaultsValues() - } - - func reloadUserDefaults() { - CacheProvider.shared.reload() - } - -} - -// MARK: - Private - -private extension UserDefaultsSettingsViewModel { - - func bindState() { - CacheProvider.shared.$userDefaultValues - .map { Array($0).sorted(by: { $0.key < $1.key }) } - .assign(to: \.userDefaultValues, on: self) - .store(in: &cancellables) - } - -} diff --git a/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerView.swift b/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerView.swift index 2f1bfe8..9164793 100644 --- a/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerView.swift +++ b/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerView.swift @@ -6,15 +6,29 @@ // import SwiftUI +import Factory struct UserProfilesPickerView: View { - // MARK: - State - @ObservedObject private var viewModel = UserProfilesPickerViewModel() + // MARK: - Cached + + @AppStorage("testingUsers", store: UserDefaults(suiteName: Constants.suiteName)) + private(set) var userProfiles: [UserProfile] = [] + + @AppStorage("selectedUserUser", store: UserDefaults(suiteName: Constants.suiteName)) + private(set) var selectedUserProfile: UserProfile? + + // MARK: - Observed + + @ObservedObject private var viewModel: UserProfilesPickerViewModel // MARK: - Body + init(viewModel: UserProfilesPickerViewModel) { + self.viewModel = viewModel + } + var body: some View { content() .navigationTitle("User profiles picker") @@ -133,7 +147,7 @@ private extension UserProfilesPickerView { func userProfileItemView(userProfile: UserProfile) -> some View { Button { - viewModel.setSelectedUserProfile(userProfile: userProfile) + self.selectedUserProfile = userProfile } label: { HStack { VStack(alignment: .leading) { @@ -146,7 +160,7 @@ private extension UserProfilesPickerView { Spacer() - if userProfile == viewModel.selectedUserProfile { + if userProfile == selectedUserProfile { Image(systemName: "checkmark.circle") .foregroundColor(SwiftUI.Color.green) } @@ -214,12 +228,8 @@ private extension UserProfilesPickerView { } -// MARK: - Previews +#Preview { -struct TestingUserPickerView_Previews: PreviewProvider { - - static var previews: some View { - UserProfilesPickerView() - } + UserProfilesPickerView(viewModel: .init()) } diff --git a/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerViewModel.swift b/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerViewModel.swift index d176128..be0b3c9 100644 --- a/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerViewModel.swift +++ b/Sources/AppDebugMode/Screens/UserProfilesPickerView/UserProfilesPickerViewModel.swift @@ -8,18 +8,20 @@ import SwiftUI final class UserProfilesPickerViewModel: ObservableObject { - + + // MARK: - Cached + + @AppStorage("testingUsers", store: UserDefaults(suiteName: Constants.suiteName)) + private(set) var userProfiles: [UserProfile] = [] + // MARK: - Properties @Published var isFileImporterPresented = false - @Published var userProfiles = UserProfilesProvider.shared.userProfiles - @Published var selectedUserProfile = UserProfilesProvider.shared.selectedUserProfile @Published var hasValidationError = false @Published var isShowingCopiedAlert = false @Published var hintModalIsPresented = false // MARK: - Constants - let jsonExample = """ [ { @@ -32,13 +34,10 @@ final class UserProfilesPickerViewModel: ObservableObject { } ] """ - + + init() {} + // MARK: - Methods - - func setSelectedUserProfile(userProfile: UserProfile) { - selectedUserProfile = userProfile - UserProfilesProvider.shared.setSelectedUserProfile(userProfile: userProfile) - } func copyExampleJson() { UIPasteboard.general.string = jsonExample @@ -55,7 +54,6 @@ final class UserProfilesPickerViewModel: ObservableObject { let data = try Data(contentsOf: url) userProfiles = try decoder.decode([UserProfile].self, from: data) - UserProfilesProvider.shared.setUserProfiles(userProfiles: userProfiles) } catch { hasValidationError = true } diff --git a/Sources/AppDebugMode/Utils/AppDebugColors.swift b/Sources/AppDebugMode/Utils/AppDebugColors.swift index 2a0ea70..d20d4ce 100644 --- a/Sources/AppDebugMode/Utils/AppDebugColors.swift +++ b/Sources/AppDebugMode/Utils/AppDebugColors.swift @@ -9,14 +9,15 @@ import SwiftUI enum AppDebugColors { - static let backgroundPrimary = Color(hex: "#000000") - static let backgroundSecondary = Color(hex: "#282833") - static let primary = Color(hex: "#CFE838") - static let secondary = Color(hex: "#1F0CF2") - - static let black = Color(hex: "#111117") - - static let textPrimary = Color(hex: "#FFFFFF") - static let textSecondary = Color(hex: "#A5ABBA") - + static let primary = Color(#colorLiteral(red: 0.8445237279, green: 0.9138714671, blue: 0.2786209583, alpha: 1)) + static let secondary = Color(#colorLiteral(red: 0.1686843038, green: 0.2027454674, blue: 0.9607277513, alpha: 1)) + + static let textPrimary = Color(#colorLiteral(red: 1, green: 0.9999999404, blue: 1, alpha: 1)) + static let textSecondary = Color(#colorLiteral(red: 0.7056729198, green: 0.7281373143, blue: 0.7778759599, alpha: 1)) + + static let backgroundPrimary = Color(#colorLiteral(red: 0, green: 0, blue: 0, alpha: 1)) + static let backgroundSecondary = Color(#colorLiteral(red: 0.2080399692, green: 0.2099102139, blue: 0.2606674135, alpha: 1)) + + static let black = Color(#colorLiteral(red: 0.08346965164, green: 0.08476512879, blue: 0.1187344268, alpha: 1)) + } diff --git a/Sources/AppDebugMode/Utils/AppDebugModeLogger.swift b/Sources/AppDebugMode/Utils/AppDebugModeLogger.swift new file mode 100644 index 0000000..8de5262 --- /dev/null +++ b/Sources/AppDebugMode/Utils/AppDebugModeLogger.swift @@ -0,0 +1,38 @@ +// +// AppDebugModeLogger.swift +// AppDebugMode +// +// Created by Andrej Jasso on 27/09/2024. +// + +import GoodLogger +import OSLog +import SwiftUI +import Factory + +public struct AppDebugModeLogger: GoodLogger { + + // MARK: - Cached + + @AppStorage("capturedOutput", store: UserDefaults(suiteName: Constants.suiteName)) + var capturedOutput: [Log] = [] + + public init() {} + + nonisolated public func log(level: OSLogType, message: String, privacy: PrivacyType) { + capturedOutput.append(Log(message: message)) + sendToAllPeers(message: message) + } + + func sendToAllPeers(message: String) { +#warning("Crashing the app todo: Fix") + Task { @MainActor in + do { + try Container.shared.sessionManager.resolve().send(message) + } catch { + print("Error sending message: \(error)") + } + } + } + +} diff --git a/Sources/AppDebugMode/Utils/Constants.swift b/Sources/AppDebugMode/Utils/Constants.swift index 665cf09..208f3e4 100644 --- a/Sources/AppDebugMode/Utils/Constants.swift +++ b/Sources/AppDebugMode/Utils/Constants.swift @@ -5,23 +5,8 @@ // Created by Matus Klasovity on 21/09/2023. // -import Foundation +public struct Constants { -enum Constants { + public static let suiteName = "com.goodrequest.appdebugmode" - // MARK: - iOS Version - - enum iOSVersion { - - static let iOS14: Bool = { - guard #available(iOS 15, *) else { - // If it's iOS 14 return true. - return true - } - // If it's iOS 15 or higher, return false. - return false - }() - - } - } diff --git a/Sources/AppDebugMode/Utils/DebugSeverSelector.swift b/Sources/AppDebugMode/Utils/DebugSeverSelector.swift new file mode 100644 index 0000000..e8c1033 --- /dev/null +++ b/Sources/AppDebugMode/Utils/DebugSeverSelector.swift @@ -0,0 +1,108 @@ +// +// BaseUrlProvider.swift +// AppDebugMode +// +// Created by Andrej Jasso on 24/09/2024. +// + +import Foundation +import GoodNetworking + +enum CustomServerError: Error { + + case invalidUrl + case repeatedName + +} + +public actor DebugSelectableServerProvider: ObservableObject, Identifiable, Hashable { + + nonisolated public func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + public static func == (lhs: DebugSelectableServerProvider, rhs: DebugSelectableServerProvider) -> Bool { + lhs.id == rhs.id + } + + // MARK: - Properties + + public let id: String = UUID().uuidString + + // MARK: - State + + private let userDefaults = UserDefaults(suiteName: "CustomBaseUrlProvider") + public var serverCollection: ApiServerCollection + public var onServerChange: CheckedContinuation? + public var customServers: [ApiServer] = [] + public var selectedServerName: String = "" + + // MARK: - Initializer + + public init(apiServerPickerConfiguration: ApiServerPickerConfiguration) { + self.serverCollection = apiServerPickerConfiguration.serversCollections + self.onServerChange = apiServerPickerConfiguration.onSelectedServerChange + + if let userDefaults { + if let customServers = try? userDefaults.getObject(forKey: "CustomServers", castTo: [ApiServer].self) { + self.customServers = customServers + } + + if let selectedServerName = try? userDefaults.getObject(forKey: serverCollection.name, castTo: String.self) { + self.selectedServerName = selectedServerName + } + } + } + + // MARK: - Methods + + public func addCustomServer(customServerUrlString: String, customName: String, collectionName: String) throws { + guard !customServers.contains(where: { $0.name == customName }) else { + throw CustomServerError.repeatedName + } + + let urlPredicate = NSPredicate(format: "SELF MATCHES %@", "^https://[A-Za-z0-9.-]{2,}\\.[A-Za-z]{2,}(?:/[^\\s]*)?$") + if urlPredicate.evaluate(with: customServerUrlString) { + let customServer = ApiServer(name: customName, url: customServerUrlString) + customServers.append(customServer) + saveToUserDefaults(customServers, key: "CustomServers") + } else { + throw CustomServerError.invalidUrl + } + } + + public func getSelectedServer() -> ApiServer { + if let selectedServer = serverCollection.servers.first(where: { $0.name == selectedServerName }) { + return selectedServer + } else if let selectedServer = customServers.first(where: { $0.name == selectedServerName }){ + return selectedServer + } + + return serverCollection.defaultServer + } + + public func deleteCustomServer(_ customServer: ApiServer) { + customServers.removeAll(where: { $0.name == customServer.name }) + saveToUserDefaults(customServers, key: "CustomServers") + } + + public func setSelectedServer(_ server: ApiServer) { + self.selectedServerName = server.name + saveToUserDefaults(server.name, key: serverCollection.name) + } + + private func saveToUserDefaults(_ object: Encodable, key: String) { + do { + try userDefaults?.setObject(object, forKey: key) + } catch { + print(error) + } + } + +} + +extension DebugSelectableServerProvider: BaseUrlProviding { + + public func resolveBaseUrl() async -> String? { getSelectedServer().url } + +} diff --git a/Sources/AppDebugMode/Utils/GRHaptics/GRHapticsManager.swift b/Sources/AppDebugMode/Utils/GRHaptics/GRHapticsManager.swift new file mode 100644 index 0000000..32d1108 --- /dev/null +++ b/Sources/AppDebugMode/Utils/GRHaptics/GRHapticsManager.swift @@ -0,0 +1,153 @@ +// +// GRHapticsManager.swift +// GoodExtensions-iOS +// +// Created by Marek Vrican on 20/02/2023. +// Copyright © GoodRequest s.r.o. All rights reserved. +// + +import UIKit +import CoreHaptics + +/// The `GRHapticsManager` class manages haptic feedback for a user interface. +@MainActor +public final class GRHapticsManager { + + /// An enumeration representing different levels of impact feedback. + public enum ImpactGeneratorStyle { + + case light + case medium + case heavy + + } + + // MARK: - Constants + + /// A singleton instance of the `GRHapticsManager` class. + public static let shared = GRHapticsManager() + + /// A `UINotificationFeedbackGenerator` that generates notification feedback. + private let notificationGenerator = UINotificationFeedbackGenerator() + + /// A `UISelectionFeedbackGenerator` that generates selection feedback. + private let selectionGenerator = UISelectionFeedbackGenerator() + + /// `UIImpactFeedbackGenerator`s that generate impact feedback at different levels. + private let impactGenerators = ( + light: UIImpactFeedbackGenerator(style: .light), + medium: UIImpactFeedbackGenerator(style: .medium), + heavy: UIImpactFeedbackGenerator(style: .heavy) + ) + + /// The haptic engine used for custom haptic patterns. + private var engine: CHHapticEngine? + + /// A boolean value indicating whether the device supports haptic feedback. + private var supportsHaptics: Bool = false + + // MARK: - Initializer + + /// Initializes the haptic engine and sets the `supportsHaptics` property based on whether the device supports haptic feedback. + private init() { + initHapticEngine() + } + +} + +// MARK: - Public + +public extension GRHapticsManager { + + /// Plays a haptic feedback with the given intensity, sharpness, and duration. + /// + /// - Parameters: + /// - intensity: The intensity of the haptic feedback. Valid values are between 0 and 1. + /// - sharpness: The sharpness of the haptic feedback. Valid values are between 0 and 1. + /// - duration: The duration of the haptic feedback in seconds. + /// + /// This method requires a device that supports haptic feedback. If the device does not support haptic feedback, this method does nothing. + func playHapticFeedback(intensity: Float, sharpness: Float, duration: TimeInterval) { + guard supportsHaptics else { return } + + let sharpnessParameter = CHHapticEventParameter(parameterID: .hapticSharpness, value: sharpness) + let intensityParameter = CHHapticEventParameter(parameterID: .hapticIntensity, value: intensity) + + let hapticEvent = CHHapticEvent( + eventType: .hapticTransient, + parameters: [intensityParameter, sharpnessParameter], + relativeTime: 0, + duration: duration + ) + + playCustomHaptics(events: [hapticEvent]) + } + + /// Plays a notification feedback with the given feedback type. + /// + /// - Parameter feedback: The feedback type to play. + func playNotificationFeedback(_ feedback: UINotificationFeedbackGenerator.FeedbackType) { + notificationGenerator.notificationOccurred(feedback) + } + + /// Plays a custom haptic pattern. + /// + /// - Parameter pattern: The haptic pattern to play. + func playCustomPattern(pattern: GRHapticsPattern) { + playCustomHaptics(events: pattern.events) + } + + /// Plays a selection feedback. + func playSelectionFeedback() { + selectionGenerator.selectionChanged() + } + + /// Plays an impact feedback with the given style. + /// + /// - Parameter style: The style of the impact feedback. + func playImpactFeedback(style: ImpactGeneratorStyle) { + switch style { + case .light: + impactGenerators.light.impactOccurred() + + case .medium: + impactGenerators.medium.impactOccurred() + + case .heavy: + impactGenerators.heavy.impactOccurred() + } + } + +} + +// MARK: - Private + +private extension GRHapticsManager { + + /// Initializes the haptic engine and sets the `supportsHaptics` property based on whether the device supports haptic feedback. + private func initHapticEngine() { + guard CHHapticEngine.capabilitiesForHardware().supportsHaptics else { return } + + do { + engine = try CHHapticEngine() + try engine?.start() + supportsHaptics = true + } catch { + print("⚠️ Error starting haptic engine: \(error.localizedDescription)") + } + } + + /// Plays a custom haptic pattern using the `engine` property. + private func playCustomHaptics(events: [CHHapticEvent]) { + guard supportsHaptics else { return } + + do { + let pattern = try CHHapticPattern(events: events, parameters: []) + let player = try engine?.makePlayer(with: pattern) + try player?.start(atTime: 0) + } catch { + print("⚠️ Failed to play pattern: \(error.localizedDescription).") + } + } + +} diff --git a/Sources/AppDebugMode/Utils/GRHaptics/GRHapticsPattern.swift b/Sources/AppDebugMode/Utils/GRHaptics/GRHapticsPattern.swift new file mode 100644 index 0000000..56deac5 --- /dev/null +++ b/Sources/AppDebugMode/Utils/GRHaptics/GRHapticsPattern.swift @@ -0,0 +1,39 @@ +// +// GRHapticsPattern.swift +// GoodExtensions-iOS +// +// Created by Marek Vrican on 20/02/2023. +// Copyright © GoodRequest s.r.o. All rights reserved. +// + +import CoreHaptics + +/// A protocol for defining a custom haptic pattern. +public protocol GRHapticsPattern { + + /// An array of CHHapticEvent objects that defines the custom haptic pattern. + var events: [CHHapticEvent] { get } + +} + +enum HapticsPattern: GRHapticsPattern { + + case peerDiscovery + + var events: [CHHapticEvent] { + switch self { + case .peerDiscovery: + return [ + CHHapticEvent (eventType: .hapticTransient, parameters: [], relativeTime: 0), + CHHapticEvent (eventType: .hapticTransient, parameters: [], relativeTime: 0.2), + CHHapticEvent (eventType: .hapticTransient, parameters: [], relativeTime: 0.4), + CHHapticEvent (eventType: .hapticContinuous, parameters: [], relativeTime: 0.6, duration: 0.5), + CHHapticEvent (eventType: .hapticContinuous, parameters: [], relativeTime: 1.2, duration: 0.5), + CHHapticEvent (eventType: .hapticContinuous, parameters: [], relativeTime: 1.8, duration: 0.5), + CHHapticEvent (eventType: .hapticTransient, parameters: [], relativeTime: 2.4), + CHHapticEvent (eventType: .hapticTransient, parameters: [], relativeTime: 2.6), + CHHapticEvent (eventType: .hapticTransient, parameters: [], relativeTime: 2.8), + ] + } + } +} diff --git a/Sources/AppDebugMode/Views/AppDebugView.swift b/Sources/AppDebugMode/Views/AppDebugView.swift index 2cb1ba3..81b08ec 100644 --- a/Sources/AppDebugMode/Views/AppDebugView.swift +++ b/Sources/AppDebugMode/Views/AppDebugView.swift @@ -6,18 +6,34 @@ // import SwiftUI +import Factory +import PulseUI struct AppDebugView: View { + // MARK: - Factory + + @Injected(\.packageManager) private var packageManager: PackageManager + @Injected(\.profileProvider) private var profileProvider: UserProfilesProvider + @Injected(\.apnsProviding) private var apnsProvider: PushNotificationsProvider? + + // MARK: - Environtment + @Environment(\.presentationMode) var presentationMode @Environment(\.hostingControllerHolder) var viewControlleeHolder - @EnvironmentObject var connectionsManager: ConnectionsManager + + // MARK: - State + + @State private var userDefaultValues: [String : String] = [:] + @State private var keychainValues: [String : String] = [:] // MARK: - Properties - private var screens: [Screen] - let customControls: CustomControls - let customControlsViewIsVisible: Bool + private var screens: [Screen] = [] + private var customControls: CustomControls + private var customControlsViewIsVisible: Bool + + // MARK: - Structs struct Screen { @@ -29,50 +45,38 @@ struct AppDebugView: View { // MARK: - Init - init(serversCollections: [ApiServerCollection], customControls: CustomControls, customControlsViewIsVisible: Bool) { - self.screens = [] + init( + customControls: CustomControls, + customControlsViewIsVisible: Bool + ) { + self.customControls = customControls + self.customControlsViewIsVisible = customControlsViewIsVisible - if !AppDebugModeProvider.shared.serversCollections.isEmpty { - self.screens.append(Screen( - title: "Server settings", - image: Image(systemName: "server.rack"), - destination: AnyView(ServersCollectionsView( - viewModel: ServersCollectionsViewModel( - serversCollections: serversCollections - ) - )) - )) - } + self.screens.append(Screen( + title: "Server settings", + image: Image(systemName: "server.rack"), + destination: AnyView(ServersCollectionsView()) + )) self.screens.append(Screen( title: "User profiles", image: Image(systemName: "person.2"), - destination: AnyView(UserProfilesPickerView()) + destination: AnyView(UserProfilesPickerView(viewModel: .init())) )) - if let pushNotificationsProvider = AppDebugModeProvider.shared.pushNotificationsProvider { + if let apnsProvider { self.screens.append(Screen( title: "Push notifications", image: Image(systemName: "bell.badge"), - destination: AnyView(PushNotificationsSettingsView(pushNotificationsProvider: pushNotificationsProvider)) - )) - } - - if !CacheProvider.shared.userDefaultValues.isEmpty { - self.screens.append(Screen( - title: "User defaults", - image: Image(systemName: "externaldrive"), - destination: AnyView(UserDefaultsSettingsView()) + destination: AnyView(PushNotificationsSettingsView(pushNotificationsProvider: apnsProvider)) )) } - if !CacheProvider.shared.keychainValues.isEmpty { - self.screens.append(Screen( - title: "Keychain settings", - image: Image(systemName: "key"), - destination: AnyView(KeychainSettingsView()) - )) - } + self.screens.append(Screen( + title: "Pulse Logs", + image: Image(systemName: "wave.3.forward"), + destination: AnyView(PulseUI.ConsoleView().tint(AppDebugColors.primary)) + )) self.screens.append(contentsOf: [ Screen( @@ -83,19 +87,19 @@ struct AppDebugView: View { Screen( title: "Logs", image: Image(systemName: "list.bullet.rectangle"), - destination: AnyView(ConsoleLogsView()) - ), + destination: AnyView(ConsoleLogsListView()) + ) + ]) + + self.screens.append( Screen( title: "Connections", image: Image(systemName: "network"), destination: AnyView(erasing: ConnectionsSettingsView()) ) - ]) - - self.customControls = customControls - self.customControlsViewIsVisible = customControlsViewIsVisible + ) } - + // MARK: - Body @@ -119,6 +123,7 @@ struct AppDebugView: View { appearanceProxy.scrollEdgeAppearance = standardAppearance appearanceProxy.standardAppearance = standardAppearance } + } } @@ -144,7 +149,7 @@ private extension AppDebugView { @ViewBuilder func navigationLink(screen: Screen) -> some View { Button { - let viewController = screen.destination.environmentObject(connectionsManager).eraseToUIViewController() + let viewController = screen.destination.eraseToUIViewController() viewControlleeHolder?.controller?.navigationController?.pushViewController(viewController, animated: false) } label: { HStack { diff --git a/Sources/AppDebugMode/Views/Buttons/ButtonFilled.swift b/Sources/AppDebugMode/Views/Buttons/ButtonFilled.swift index 5a3a71b..484e947 100644 --- a/Sources/AppDebugMode/Views/Buttons/ButtonFilled.swift +++ b/Sources/AppDebugMode/Views/Buttons/ButtonFilled.swift @@ -8,21 +8,24 @@ import SwiftUI struct ButtonFilled: View { - + enum ButtonStyle { case primary case secondary case danger - + + private static let primaryForegroundColor = #colorLiteral(red: 0.08346965164, green: 0.08476512879, blue: 0.1187344268, alpha: 1) + private static let secondaryForegroundColor = #colorLiteral(red: 0.97188586, green: 0.975427568, blue: 0.9876087308, alpha: 1) + var foregroundColor: Color { switch self { case .primary: - return Color(hex: "#111117") - + return Color(Self.primaryForegroundColor) + case .secondary: - return Color(hex: "#F6F7FB") - + return Color(Self.secondaryForegroundColor) + case .danger: return .white } diff --git a/Tests/AppDebugModeTests/AppDebugModeTests.swift b/Tests/AppDebugModeTests/AppDebugModeTests.swift index 102c01b..79fc0f2 100644 --- a/Tests/AppDebugModeTests/AppDebugModeTests.swift +++ b/Tests/AppDebugModeTests/AppDebugModeTests.swift @@ -6,6 +6,6 @@ final class AppDebugModeTests: XCTestCase { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct // results. - XCTAssertEqual(AppDebugMode().text, "Hello, World!") +// XCTAssertEqual(AppDebugMode.text, "Hello, World!") } }