diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index fbc6c40..79a58f7 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -7,6 +7,11 @@ objects = { /* Begin PBXBuildFile section */ + 7A06E0AC2C7065AC005731AC /* SettingsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0AB2C7065AB005731AC /* SettingsScreen.swift */; }; + 7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0AD2C7065C7005731AC /* SettingsViewModel.swift */; }; + 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0AF2C7065D8005731AC /* SettingsCoordinator.swift */; }; + 7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0B22C707E13005731AC /* SettingsServiceProtocol.swift */; }; + 7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0B42C707E2B005731AC /* SettingsService.swift */; }; 7A1022692C55197D00B84627 /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7ADF23052C25B5BF002624FF /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226B2C551EC500B84627 /* LocationEditScreen.swift */; }; 7A10226E2C551EE000B84627 /* LocationEditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */; }; @@ -50,6 +55,7 @@ 7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33381024990DAE00D878F1 /* FiltersController.swift */; }; 7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3399AA299063370087DF98 /* SearchControllerExt.swift */; }; 7A35177B27E23F8800DC538C /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = 7A35177A27E23F8800DC538C /* Eureka */; }; + 7A3E12D72C7B42B700EE710D /* UserDefaults+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */; }; 7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E30F22C18840600567704 /* ActivityItemSource.swift */; }; 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; }; 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; }; @@ -59,6 +65,7 @@ 7A599C362C18AC7F00D47C18 /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C352C18AC7F00D47C18 /* ApiError.swift */; }; 7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */; }; 7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */; }; + 7A5D7E0C2C71EB25002C17E7 /* ToggleRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D7E0B2C71EB25002C17E7 /* ToggleRowView.swift */; }; 7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84B82C1AD3C200C2209B /* DtoConvertible.swift */; }; 7A5D84BC2C1AD81000C2209B /* VehicleOwnershipPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84BB2C1AD81000C2209B /* VehicleOwnershipPeriod.swift */; }; 7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84BD2C1AE44700C2209B /* VehiclePhoto.swift */; }; @@ -161,7 +168,6 @@ 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; }; 7AE26A3524F31B0700625033 /* EventsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3424F31B0700625033 /* EventsController.swift */; }; 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */; }; - 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; }; 7AF6D2042677C03B0086EA64 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; }; 7AF6D2052677C03B0086EA64 /* AutoCatCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7AF6D2122677C12E0086EA64 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A000AA124C2EEDE001F5B00 /* Location.swift */; }; @@ -242,6 +248,11 @@ 7A000AA124C2EEDE001F5B00 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 7A0420A925619AEC00034941 /* Osago.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Osago.swift; sourceTree = ""; }; 7A0516192414FF0900FC55AC /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = ""; }; + 7A06E0AB2C7065AB005731AC /* SettingsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsScreen.swift; sourceTree = ""; }; + 7A06E0AD2C7065C7005731AC /* SettingsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewModel.swift; sourceTree = ""; }; + 7A06E0AF2C7065D8005731AC /* SettingsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCoordinator.swift; sourceTree = ""; }; + 7A06E0B22C707E13005731AC /* SettingsServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsServiceProtocol.swift; sourceTree = ""; }; + 7A06E0B42C707E2B005731AC /* SettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsService.swift; sourceTree = ""; }; 7A10226B2C551EC500B84627 /* LocationEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditScreen.swift; sourceTree = ""; }; 7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditViewModel.swift; sourceTree = ""; }; 7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditCoordinator.swift; sourceTree = ""; }; @@ -291,6 +302,7 @@ 7A33381024990DAE00D878F1 /* FiltersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersController.swift; sourceTree = ""; }; 7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; 7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = ""; }; + 7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Settings.swift"; sourceTree = ""; }; 7A3E30F22C18840600567704 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = ""; }; 7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = ""; }; 7A3F07AC2436350B00E59687 /* SearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchController.swift; sourceTree = ""; }; @@ -303,6 +315,7 @@ 7A599C352C18AC7F00D47C18 /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = ""; }; 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbRefreshTokenModel.swift; sourceTree = ""; }; 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbVerifyTokenModel.swift; sourceTree = ""; }; + 7A5D7E0B2C71EB25002C17E7 /* ToggleRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleRowView.swift; sourceTree = ""; }; 7A5D84B82C1AD3C200C2209B /* DtoConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DtoConvertible.swift; sourceTree = ""; }; 7A5D84BB2C1AD81000C2209B /* VehicleOwnershipPeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleOwnershipPeriod.swift; sourceTree = ""; }; 7A5D84BD2C1AE44700C2209B /* VehiclePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhoto.swift; sourceTree = ""; }; @@ -409,7 +422,6 @@ 7AE26A3424F31B0700625033 /* EventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsController.swift; sourceTree = ""; }; 7AE8424D26109F78002F6B31 /* Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exportable.swift; sourceTree = ""; }; 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = ""; }; - 7AEFE727240455E200910EB7 /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AutoCatCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AutoCatCore.h; sourceTree = ""; }; 7AF6D1F22677C03B0086EA64 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -472,6 +484,26 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 7A06E0AA2C706550005731AC /* SettingsScreen */ = { + isa = PBXGroup; + children = ( + 7A06E0AB2C7065AB005731AC /* SettingsScreen.swift */, + 7A06E0AD2C7065C7005731AC /* SettingsViewModel.swift */, + 7A06E0AF2C7065D8005731AC /* SettingsCoordinator.swift */, + ); + path = SettingsScreen; + sourceTree = ""; + }; + 7A06E0B12C707DD7005731AC /* SettingsService */ = { + isa = PBXGroup; + children = ( + 7A06E0B22C707E13005731AC /* SettingsServiceProtocol.swift */, + 7A06E0B42C707E2B005731AC /* SettingsService.swift */, + 7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */, + ); + path = SettingsService; + sourceTree = ""; + }; 7A0B969D257D6CB3000B39AD /* eureka */ = { isa = PBXGroup; children = ( @@ -564,7 +596,6 @@ 7A27ADC6249D43210035F39E /* RegionsController.swift */, 7A11471723FDEBFA00B424AF /* ReportController.swift */, 7A3F07AC2436350B00E59687 /* SearchController.swift */, - 7AEFE727240455E200910EB7 /* SettingsController.swift */, 7AC3554B29696A1C00889457 /* MainTabController.swift */, 7AC3554D29696C4500889457 /* DummyNewController.swift */, 7AC3554F29696D5A00889457 /* NewNumberController.swift */, @@ -628,6 +659,7 @@ 7A1441632C297E9800E79018 /* Screens */ = { isa = PBXGroup; children = ( + 7A06E0AA2C706550005731AC /* SettingsScreen */, 7A1022752C557E3F00B84627 /* LocationPickerScreen */, 7A10226A2C551EA200B84627 /* LocationEditScreen */, 7A71580A2C44451B00852088 /* AdsScreen */, @@ -680,6 +712,7 @@ 7A45FB362C2706D000618694 /* Services */ = { isa = PBXGroup; children = ( + 7A06E0B12C707DD7005731AC /* SettingsService */, 7A60D24B2C5A9D2700D13F7B /* LocationService */, 7AB5873D2C42FF4000FA7B66 /* ApiService */, 7AB587302C42D35900FA7B66 /* StorageService */, @@ -966,6 +999,7 @@ 7A961C6D2C4C3C9E00CE2211 /* LinkRowView.swift */, 7AAAFAD92C4D1AFE0050410D /* Zoomable.swift */, 7A1022712C554A1300B84627 /* CustomHostingController.swift */, + 7A5D7E0B2C71EB25002C17E7 /* ToggleRowView.swift */, ); path = SwiftUI; sourceTree = ""; @@ -1200,6 +1234,7 @@ 7A961C6C2C4C3C8600CE2211 /* TextRowView.swift in Sources */, 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */, 7A1022772C557EC400B84627 /* LocationPickerScreen.swift in Sources */, + 7A5D7E0C2C71EB25002C17E7 /* ToggleRowView.swift in Sources */, 7A7158092C44087E00852088 /* OsagoCoordinator.swift in Sources */, 7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */, 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */, @@ -1224,6 +1259,7 @@ 7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */, 7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */, 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */, + 7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */, 7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */, 7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */, 7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */, @@ -1232,7 +1268,6 @@ 7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */, 7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */, 7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */, - 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */, 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */, 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, 7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */, @@ -1276,10 +1311,12 @@ 7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */, 7AFBE8C42C302561003C491D /* ACHudContainer.swift in Sources */, 7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */, + 7A06E0AC2C7065AC005731AC /* SettingsScreen.swift in Sources */, 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */, 7A1441702C2998B200E79018 /* Formatters.swift in Sources */, 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */, 7A71580C2C44453200852088 /* AdsScreen.swift in Sources */, + 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */, 7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */, 7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */, 7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */, @@ -1343,12 +1380,15 @@ 7AF6D21E2677C1680086EA64 /* PlateNumber.swift in Sources */, 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */, 7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */, + 7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */, + 7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */, 7AF6D21F2677C1680086EA64 /* Response.swift in Sources */, 7A60D24D2C5A9D4900D13F7B /* LocationService.swift in Sources */, 7A60D24F2C5A9DA800D13F7B /* LocationServiceProtocol.swift in Sources */, 7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */, 7A64A2032C19DA1000284124 /* VehicleDto.swift in Sources */, 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */, + 7A3E12D72C7B42B700EE710D /* UserDefaults+Settings.swift in Sources */, 7A64A2222C19E99E00284124 /* DebugInfoDto.swift in Sources */, 7A5D84BC2C1AD81000C2209B /* VehicleOwnershipPeriod.swift in Sources */, 7A64A2202C19E93500284124 /* VehicleNoteDto.swift in Sources */, @@ -1567,7 +1607,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; @@ -1594,7 +1634,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 134; + CURRENT_PROJECT_VERSION = 135; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index 17faac0..4301ff6 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -55,17 +54,17 @@ - + - + - + - + - + @@ -342,22 +341,6 @@ - - - - - - - - - - - - - - - - @@ -655,7 +638,6 @@ - @@ -928,8 +910,6 @@ - - diff --git a/AutoCat/Controllers/GoogleSignInController.swift b/AutoCat/Controllers/GoogleSignInController.swift index 62f8a7f..b771dda 100644 --- a/AutoCat/Controllers/GoogleSignInController.swift +++ b/AutoCat/Controllers/GoogleSignInController.swift @@ -45,13 +45,12 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { self.webView.load(request) } } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if let url = navigationAction.request.url { - if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) { - if let queryItems = components.queryItems { - if let code = queryItems.first(where: { $0.name == "code" })?.value { - decisionHandler(.cancel) + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy { + if let url = navigationAction.request.url { + if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) { + if let queryItems = components.queryItems { + if let code = queryItems.first(where: { $0.name == "code" })?.value { Task { @MainActor in do { @@ -62,13 +61,15 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { HUD.flash(.labeledError(title: nil, subtitle: error.localizedDescription)) } } - } - } - } - } - - decisionHandler(.allow) - } + + return .cancel + } + } + } + } + + return .allow + } @IBAction func close(_ sender: UIBarButtonItem) { self.dismiss(animated: true, completion: nil) diff --git a/AutoCat/Controllers/MainTabController.swift b/AutoCat/Controllers/MainTabController.swift index 20b67ce..2e13248 100644 --- a/AutoCat/Controllers/MainTabController.swift +++ b/AutoCat/Controllers/MainTabController.swift @@ -4,6 +4,8 @@ import AutoCatCore class MainTabController: UITabBarController, UITabBarControllerDelegate { + var settingsCoordinator: SettingsCoordinator? + override func viewDidLoad() { super.viewDidLoad() self.delegate = self @@ -14,6 +16,15 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate { viewControllers?.remove(at: 2) #endif + + Task { await addSettings() } + } + + func addSettings() async { + + let coordinator = SettingsCoordinator(tabController: self) + settingsCoordinator = coordinator + try? await coordinator.start() } func tabBarController(_ tabBarController: UITabBarController, shouldSelect viewController: UIViewController) -> Bool { diff --git a/AutoCat/Controllers/RecordsController.swift b/AutoCat/Controllers/RecordsController.swift index 310eed8..ee2660d 100644 --- a/AutoCat/Controllers/RecordsController.swift +++ b/AutoCat/Controllers/RecordsController.swift @@ -91,11 +91,10 @@ class RecordsController: UIViewController, UITableViewDelegate { var alert: UIAlertController? var url: URL! - Task { + Task { @MainActor in do { - async let locationTask = RxLocationManager.requestCurrentLocation() - async let permissionTask: () = recorder.requestPermissions() - let (event, _) = try await (locationTask, permissionTask) + let event = try await RxLocationManager.requestCurrentLocation() + try await recorder.requestPermissions() await makeStartSoundIfNeeded() diff --git a/AutoCat/Controllers/ReportController.swift b/AutoCat/Controllers/ReportController.swift index d2e066f..a80a09d 100644 --- a/AutoCat/Controllers/ReportController.swift +++ b/AutoCat/Controllers/ReportController.swift @@ -129,10 +129,6 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource } .cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator } .onCellSelection { _, row in -// let controller = AdsController() -// controller.ads = self.vehicle?.ads ?? [] -// self.navigationController?.pushViewController(controller, animated: true) - if let ads = self.vehicle?.ads, let navController = self.navigationController { let coordinator = AdsCoordinator(navController: navController, ads: ads) Task { try await coordinator.start() } diff --git a/AutoCat/Controllers/SettingsController.swift b/AutoCat/Controllers/SettingsController.swift deleted file mode 100644 index 30e285b..0000000 --- a/AutoCat/Controllers/SettingsController.swift +++ /dev/null @@ -1,151 +0,0 @@ -import UIKit -import Eureka -import AuthenticationServices -import AutoCatCore - -class SettingsController: FormViewController { - - override func viewDidLoad() { - super.viewDidLoad() - - form +++ Section() - <<< LabelRow() { row in - row.title = NSLocalizedString("Version", comment: "") - row.value = Bundle.main.fullVersion - } - - form +++ Section(NSLocalizedString("Profile", comment: "")) - <<< LabelRow("AutoCatAccount") { row in - row.title = NSLocalizedString("AutoCat Account", comment: "") - row.value = Settings.shared.user.email - } - <<< LabelRow("GoogleAccount") { row in - row.title = NSLocalizedString("Google", comment: "") - if let jwtString = Settings.shared.user.firebaseIdToken, let jwt = JWT(string: jwtString) { - row.value = jwt.payload.email - } else { - row.value = NSLocalizedString("Log In", comment: "") - } - row.cellUpdate { cell, _ in - cell.accessoryType = .disclosureIndicator - } - }.onCellSelection { cell, row in - if Settings.shared.user.firebaseIdToken != nil { - self.displayLogoutSheet(for: "GoogleAccount") - } else { - self.loginToGoogle() - } - } - - +++ Section(header: NSLocalizedString("Plate number recognition", comment: ""), footer: NSLocalizedString("Recognize plate numbers in alternative form. For example 'ЕВА 123 777' instead of 'Е123ВА 777'", comment: "")) - <<< SwitchRow("AlternateRecognition") { row in - row.title = NSLocalizedString("Alternative order", comment: "") - row.value = Settings.shared.recognizeAlternativeOrder - }.onChange { row in - if let val = row.value { - Settings.shared.recognizeAlternativeOrder = val - } - } - - +++ Section(footer: NSLocalizedString("If enabled, app will try to recognize shortened plate numbers (without region) and add default region", comment: "")) - <<< SwitchRow("ShortenedNumbers") { row in - row.title = NSLocalizedString("Shortened numbers", comment: "") - row.value = Settings.shared.recognizeShortenedNumbers - }.onChange { row in - if let val = row.value { - Settings.shared.recognizeShortenedNumbers = val - } - } - <<< TextRow("") { row in - row.title = NSLocalizedString("Default region", comment: "") - row.placeholder = "161" - row.value = Settings.shared.defaultRegion - row.hidden = Condition.function(["ShortenedNumbers"], { form in - return !((form.rowBy(tag: "ShortenedNumbers") as? SwitchRow)?.value ?? false) - }) - }.onChange { row in - if let val = row.value { - Settings.shared.defaultRegion = val - } - } - - +++ Section(footer: NSLocalizedString("When enabled, you will hear short sound before starting audio recording. This will only work when audio record is started via Siri", comment: "")) { $0.tag = "BeepRecordSection" } - <<< SwitchRow("BeepRecord") { row in - row.title = NSLocalizedString("Beep before record", comment: "") - row.value = Settings.shared.recordBeep - }.onChange{ row in - if let val = row.value { - Settings.shared.recordBeep = val - } - } - - +++ Section(NSLocalizedString("Debug", comment: "")) - <<< SwitchRow() { row in - row.title = NSLocalizedString("Show debug info", comment: "") - row.value = Settings.shared.showDebugInfo - }.onChange { row in - if let val = row.value { - Settings.shared.showDebugInfo = val - } - } - - +++ Section("") - <<< ButtonRow("SignOut") { $0.title = NSLocalizedString("Sign Out", comment: "") }.onCellSelection { cell, row in - self.logout() - } - - #if targetEnvironment(macCatalyst) - if let beepSection = self.form.sectionBy(tag: "BeepRecordSection") { - beepSection.hidden = true - beepSection.evaluateHidden() - } - #endif - } - - func logout() { - Settings.shared.user.token = "" - let storyboard = UIStoryboard(name: "Main", bundle: nil) - self.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController") - } - - func displayLogoutSheet(for row: String) { - guard let jwtString = Settings.shared.user.firebaseIdToken, let jwt = JWT(string: jwtString) else { return } - - let msg = String.localizedStringWithFormat(NSLocalizedString("You are currently signed in with email %@. It will help to gather more data about vehicles.", comment: ""), jwt.payload.email) - let sheet = UIAlertController(title: jwt.payload.name, message: msg, preferredStyle: .actionSheet) - - let cancel = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) } - let logout = UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in - Settings.shared.user.firebaseIdToken = nil - Settings.shared.user.firebaseRefreshToken = nil - if let row = self.form.rowBy(tag: row) as? LabelRow { - row.value = NSLocalizedString("Log In", comment: "") - row.reload() - } - } - - sheet.addAction(logout) - sheet.addAction(cancel) - self.present(sheet, animated: true, completion: nil) - } - - func loginToGoogle() { - let storyboard = UIStoryboard(name: "Main", bundle: nil) - if let vc = storyboard.instantiateViewController(identifier: "GoogleSignInController") as? GoogleSignInController { - vc.completion = { - guard let googleRow = self.form.rowBy(tag: "GoogleAccount") as? LabelRow else { return } - if let jwtString = Settings.shared.user.firebaseIdToken, let jwt = JWT(string: jwtString) { - googleRow.value = jwt.payload.email - } else { - googleRow.value = NSLocalizedString("Log In", comment: "") - } - googleRow.reload() - } - self.present(vc, animated: true) - } - } - - func loginToApple() { - - } -} diff --git a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift new file mode 100644 index 0000000..8dbe81a --- /dev/null +++ b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift @@ -0,0 +1,51 @@ +// +// SettingsCoordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 17.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import UIKit +import SwiftUI +import AutoCatCore + +@MainActor +class SettingsCoordinator: Coordinator { + + weak var viewController: UITabBarController? + var settingsController: UIViewController? + + init(tabController: UITabBarController) { + + self.viewController = tabController + } + + func start() async throws { + + let viewModel = SettingsViewModel(settingService: SettingsService.shared) + viewModel.coordinator = self + let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel)) + settingsController = controller + + let navController = UINavigationController(rootViewController: controller) + navController.tabBarItem = UITabBarItem(title: NSLocalizedString("Settings", comment: ""), + image: UIImage(systemName: "gear"), tag: 0) + + viewController?.viewControllers?.append(navController) + } + + func openAuthScreen() { + + let storyboard = UIStoryboard(name: "Main", bundle: nil) + viewController?.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController") + } + + func openGoogleOauthPage() { + print("===== openGoogleOauthPage") + let storyboard = UIStoryboard(name: "Main", bundle: nil) + if let vc = storyboard.instantiateViewController(identifier: "GoogleSignInController") as? GoogleSignInController { + settingsController?.present(vc, animated: true) + } + } +} diff --git a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift new file mode 100644 index 0000000..31b49a7 --- /dev/null +++ b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift @@ -0,0 +1,87 @@ +// +// SettingsScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 17.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct SettingsScreen: View { + + @State var viewModel: SettingsViewModel + @State var googleSheetOpened = false + + var body: some View { + Form { + Section { + TextRowView(title: "Version", value: Bundle.main.fullVersion) + } + + Section("Profile") { + SimpleTextRowView(title: "AutoCat Account", value: viewModel.autocatEmail) + SimpleTextRowView(title: "Google", + value: viewModel.googleEmail ?? "Log In", + showArrow: true) + .onTapGesture { + if viewModel.googleAuthorized { + googleSheetOpened = true + } else { + viewModel.googleSignIn() + } + } + + if viewModel.canSignOut { + Button("Sign Out", action: viewModel.signOut) + } + } + + Section("Plate number recognition") { + ToggleRowView(title: "Alternative order", + description: "Recognize plate numbers in alternative form. For example 'ЕВА 123 777' instead of 'Е123ВА 777'", + toggle: $viewModel.settingService.recognizeAlternativeOrder) + ToggleRowView(title: "Shortened numbers", + description: "If enabled, app will try to recognize shortened plate numbers (without region) and add default region", + toggle: $viewModel.settingService.recognizeShortenedNumbers) + if viewModel.settingService.recognizeShortenedNumbers { + LabeledContent("Default region") { + TextField("", text: $viewModel.settingService.defaultRegion) + .frame(width: 50) + .multilineTextAlignment(.trailing) + } + } + ToggleRowView(title: "Beep before record", + description: "When enabled, you will hear short sound before starting audio recording. This will only work when audio record is started via Siri", + toggle: $viewModel.settingService.recordBeep) + } + + Section("Debug") { + ToggleRowView(title: "Show debug info", + description: nil, + toggle: $viewModel.settingService.showDebugInfo) + ToggleRowView(title: "Use test backend", + description: nil, + toggle: $viewModel.settingService.useTestBackend) + } + } + .navigationTitle("Settings") + .confirmationDialog(viewModel.googleUsername ?? "", + isPresented: $googleSheetOpened, + titleVisibility: .visible) { + Button("Sign Out", role: .destructive) { + viewModel.googleSignout() + } + } message: { + if let email = viewModel.googleEmail { + Text("You are currently signed in with email \(email). It will help to gather more data about vehicles.") + } + } + + } +} + +#Preview { + SettingsScreen(viewModel: .init(settingService: SettingsService(defaults: .standard))) +} diff --git a/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift b/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift new file mode 100644 index 0000000..ac3e799 --- /dev/null +++ b/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift @@ -0,0 +1,62 @@ +// +// SettingsViewModel.swift +// AutoCat +// +// Created by Selim Mustafaev on 17.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +@MainActor +@Observable +class SettingsViewModel { + + weak var coordinator: SettingsCoordinator? + var settingService: SettingsServiceProtocol + + var autocatEmail: String { + settingService.user.email + } + + var canSignOut: Bool { + !settingService.user.token.isEmpty + } + + var googleAuthorized: Bool { + settingService.user.firebaseIdToken != nil + } + + var googleUsername: String? { + guard let jwtString = settingService.user.firebaseIdToken, + let jwt = JWT(string: jwtString) else { return nil } + + return jwt.payload.name + } + + var googleEmail: String? { + guard let jwtString = settingService.user.firebaseIdToken, + let jwt = JWT(string: jwtString) else { return nil } + + return jwt.payload.email + } + + init(settingService: SettingsServiceProtocol) { + self.settingService = settingService + } + + func signOut() { + settingService.user.token = "" + coordinator?.openAuthScreen() + } + + func googleSignIn() { + coordinator?.openGoogleOauthPage() + } + + func googleSignout() { + settingService.user.firebaseIdToken = nil + settingService.user.firebaseRefreshToken = nil + } +} diff --git a/AutoCat/SwiftUI/TextRowView.swift b/AutoCat/SwiftUI/TextRowView.swift index c14bc0d..2fcfe3b 100644 --- a/AutoCat/SwiftUI/TextRowView.swift +++ b/AutoCat/SwiftUI/TextRowView.swift @@ -12,11 +12,18 @@ struct TextRowView: View { let title: LocalizedStringKey let value: String? + let showArrow: Bool + + init(title: LocalizedStringKey, value: String?, showArrow: Bool = false) { + self.title = title + self.value = value + self.showArrow = showArrow + } var body: some View { if #available(iOS 16.0, *) { ViewThatFits(in: .horizontal) { - SimpleTextRowView(title: title, value: value) + SimpleTextRowView(title: title, value: value, showArrow: showArrow) VStack(alignment: .leading, spacing: 8) { Text(title) HStack { @@ -25,11 +32,16 @@ struct TextRowView: View { .foregroundStyle(.secondary) .multilineTextAlignment(.trailing) .padding(.leading) + if showArrow { + Image(systemName: "chevron.right") + .padding(.leading, 8) + .foregroundStyle(.secondary) + } } } } } else { - SimpleTextRowView(title: title, value: value) + SimpleTextRowView(title: title, value: value, showArrow: showArrow) } } } @@ -38,21 +50,35 @@ struct SimpleTextRowView: View { let title: LocalizedStringKey let value: String? + let showArrow: Bool + + init(title: LocalizedStringKey, value: String?, showArrow: Bool = false) { + self.title = title + self.value = value + self.showArrow = showArrow + } var body: some View { HStack(alignment: .firstTextBaseline, spacing: 0) { Text(title) Spacer() Text(value ?? "") + .lineLimit(1) .foregroundStyle(.secondary) + if showArrow { + Image(systemName: "chevron.right") + .padding(.leading, 8) + .foregroundStyle(.secondary) + } } } } #Preview { List { - TextRowView(title: "Title", value: "Value") - TextRowView(title: "Title", value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas cursus posuere varius. Maecenas diam neque, lacinia molestie tristique eget, faucibus egestas nibh.") + TextRowView(title: "Title", value: "Value", showArrow: true) + TextRowView(title: "Title", value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas cursus posuere varius. Maecenas diam neque, lacinia molestie tristique eget, faucibus egestas nibh.", showArrow: true) TextRowView(title: "Some longer title", value: "Value with long long text") + SimpleTextRowView(title: "Some longer title", value: "Value with long long text") } } diff --git a/AutoCat/SwiftUI/ToggleRowView.swift b/AutoCat/SwiftUI/ToggleRowView.swift new file mode 100644 index 0000000..50c103c --- /dev/null +++ b/AutoCat/SwiftUI/ToggleRowView.swift @@ -0,0 +1,57 @@ +// +// ToggleRowView.swift +// AutoCat +// +// Created by Selim Mustafaev on 18.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +extension VerticalAlignment { + + enum CustomVerticalAlignment: AlignmentID { + + static func defaultValue(in context: ViewDimensions) -> CGFloat { + context[.top] + } + } + + static let customTop = VerticalAlignment(CustomVerticalAlignment.self) +} + +struct ToggleRowView: View { + + let title: LocalizedStringKey + let description: LocalizedStringKey? + let toggle: Binding + + var body: some View { + HStack(alignment: .customTop) { + VStack(alignment: .leading) { + Text(title) + .alignmentGuide(.customTop, computeValue: { $0[.firstTextBaseline] }) + if let description { + Text(description) + .font(.footnote) + .foregroundStyle(.secondary) + } + } + Spacer() + Toggle(isOn: toggle) { + Text(" ") + .alignmentGuide(.customTop, computeValue: { $0[.firstTextBaseline] }) + } + .fixedSize() + } + } +} + +#Preview { + List { + ToggleRowView(title: "Title", + description: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas cursus posuere varius. Maecenas diam neque, lacinia molestie tristique eget, faucibus egestas nibh.", + toggle: .constant(true)) + ToggleRowView(title: "Title", description: nil, toggle: .constant(true)) + } +} diff --git a/AutoCat/Utils/Recorder.swift b/AutoCat/Utils/Recorder.swift index c7c014d..9c6b99a 100644 --- a/AutoCat/Utils/Recorder.swift +++ b/AutoCat/Utils/Recorder.swift @@ -5,7 +5,7 @@ import AudioToolbox import os.log import ExceptionCatcher -class Recorder { +final class Recorder { let engine = AVAudioEngine() var fileRef: ExtAudioFileRef? = nil diff --git a/AutoCatCore/Models/User.swift b/AutoCatCore/Models/User.swift index b00306f..3901307 100644 --- a/AutoCatCore/Models/User.swift +++ b/AutoCatCore/Models/User.swift @@ -6,8 +6,8 @@ public struct User: Codable, Sendable { public var firebaseIdToken: String? public var firebaseRefreshToken: String? - public init() { - self.email = "" - self.token = "" + public init(email: String = "", token: String = "") { + self.email = email + self.token = token } } diff --git a/AutoCatCore/Services/ApiService/ApiService.swift b/AutoCatCore/Services/ApiService/ApiService.swift index 21f54d3..dc7965c 100644 --- a/AutoCatCore/Services/ApiService/ApiService.swift +++ b/AutoCatCore/Services/ApiService/ApiService.swift @@ -2,7 +2,13 @@ import Foundation public actor ApiService: ApiServiceProtocol { - public static let shared = ApiService() + public static let shared = ApiService(settingsService: SettingsService.shared) + + let settingsService: SettingsServiceProtocol + + init(settingsService: SettingsServiceProtocol) { + self.settingsService = settingsService + } private let session: URLSession = { let sessionConfig = URLSessionConfiguration.default @@ -13,12 +19,11 @@ public actor ApiService: ApiServiceProtocol { // MARK: - Private wrappres - private func genError(_ msg: String, suggestion: String, code: Int = 0) -> Error { - return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion]) - } - private func createRequest(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible { - guard var urlComponents = URLComponents(string: Constants.baseUrl + api) else { return nil } + + let baseUrl = settingsService.useTestBackend ? Constants.baseUrlDebug : Constants.baseUrl + + guard var urlComponents = URLComponents(string: baseUrl + api) else { return nil } if let params = params, method.uppercased() == "GET" { urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: String($1)) } @@ -42,9 +47,9 @@ public actor ApiService: ApiServiceProtocol { } private func makeRequest(api: String, - method: String = "GET", - body: B?, - params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible { + method: String = "GET", + body: B?, + params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible { guard let request = self.createRequest(api: api, method: method, body: body, params: params) else { throw ApiError.generic diff --git a/AutoCatCore/Services/SettingsService/SettingsService.swift b/AutoCatCore/Services/SettingsService/SettingsService.swift new file mode 100644 index 0000000..f61da7c --- /dev/null +++ b/AutoCatCore/Services/SettingsService/SettingsService.swift @@ -0,0 +1,122 @@ +// +// SettingsService.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 17.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +@Observable +public final class SettingsService: SettingsServiceProtocol { + + public static let shared = SettingsService(defaults: .standard) + + let defaults: UserDefaults + var observations: [NSKeyValueObservation] = [] + + let jsonEncoder = JSONEncoder() + let jsonDecoder = JSONDecoder() + + public var user: User { + get { + if let data = userData { + let result = try? jsonDecoder.decode(User.self, from: data) + return result ?? User() + } else { + return User() + } + } + + set { + if let json = try? jsonEncoder.encode(newValue) { + userData = json + } + } + } + + public var userData: Data? = nil { + didSet { + defaults.user = userData + } + } + + public var recognizeAlternativeOrder: Bool = false { + didSet { + defaults.recognizeAlternativeOrder = recognizeAlternativeOrder + } + } + + public var recognizeShortenedNumbers: Bool = false { + didSet { + defaults.recognizeShortenedNumbers = recognizeShortenedNumbers + } + } + + public var defaultRegion: String = "161" { + didSet { + defaults.defaultRegion = defaultRegion + } + } + + public var recordBeep: Bool = false { + didSet { + defaults.recordBeep = recordBeep + } + } + + public var showDebugInfo: Bool = false { + didSet { + defaults.showDebugInfo = showDebugInfo + } + } + + public var useTestBackend: Bool = false { + didSet { + defaults.useTestBackend = useTestBackend + } + } + + public init(defaults: UserDefaults) { + self.defaults = defaults + + observe(key: \.recognizeAlternativeOrder, for: \.recognizeAlternativeOrder) + observe(key: \.recognizeShortenedNumbers, for: \.recognizeShortenedNumbers) + observe(key: \.defaultRegion, for: \.defaultRegion) + observe(key: \.recordBeep, for: \.recordBeep) + observe(key: \.showDebugInfo, for: \.showDebugInfo) + observe(key: \.useTestBackend, for: \.useTestBackend) + observe(key: \.user, for: \.userData) + + register(defaultValues: [ + .recognizeAlternativeOrder: false, + .recognizeShortenedNumbers: false, + .defaultRegion: "761", + .recordBeep: false, + .showDebugInfo: false, + .useTestBackend: false + ]) + } + + deinit { + observations.forEach { $0.invalidate() } + } + + func observe(key userDefaultsKey: KeyPath, for settingsKey: ReferenceWritableKeyPath) where T: Equatable { + let observation = defaults.observe(userDefaultsKey, options: [.initial, .new]) { [weak self] _, change in + guard let self else { return } + + if let new = change.newValue, self[keyPath: settingsKey] != new { + self[keyPath: settingsKey] = new + } + } + observations.append(observation) + } + + func register(defaultValues: [SettingsKey: Any]) { + defaults.register(defaults: defaultValues.reduce(into: [:], { (partialResult: inout [String: Any], tuple) in + partialResult[tuple.key.rawValue] = tuple.value + })) + } +} diff --git a/AutoCatCore/Services/SettingsService/SettingsServiceProtocol.swift b/AutoCatCore/Services/SettingsService/SettingsServiceProtocol.swift new file mode 100644 index 0000000..722ec0f --- /dev/null +++ b/AutoCatCore/Services/SettingsService/SettingsServiceProtocol.swift @@ -0,0 +1,21 @@ +// +// SettingsServiceProtocol.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 17.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public protocol SettingsServiceProtocol { + + var user: User { get set } + + var recognizeAlternativeOrder: Bool { get set } + var recognizeShortenedNumbers: Bool { get set } + var defaultRegion: String { get set } + var recordBeep: Bool { get set } + var showDebugInfo: Bool { get set } + var useTestBackend: Bool { get set } +} diff --git a/AutoCatCore/Services/SettingsService/UserDefaults+Settings.swift b/AutoCatCore/Services/SettingsService/UserDefaults+Settings.swift new file mode 100644 index 0000000..4e40757 --- /dev/null +++ b/AutoCatCore/Services/SettingsService/UserDefaults+Settings.swift @@ -0,0 +1,74 @@ +// +// UserDefaults+Settings.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 25.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +enum SettingsKey: String { + + case user + case recognizeAlternativeOrder + case recognizeShortenedNumbers + case defaultRegion + case recordBeep + case showDebugInfo + case useTestBackend +} + +extension UserDefaults { + + func bool(for key: SettingsKey) -> Bool { + bool(forKey: key.rawValue) + } + + func string(for key: SettingsKey, defaultValue: String = "") -> String { + string(forKey: key.rawValue) ?? defaultValue + } + + func data(for key: SettingsKey) -> Data? { + data(forKey: key.rawValue) + } + + func set(value: T, for key: SettingsKey) { + setValue(value, forKey: key.rawValue) + } + + @objc dynamic var recognizeAlternativeOrder: Bool { + get { bool(for: .recognizeAlternativeOrder) } + set { set(value: newValue, for: .recognizeAlternativeOrder) } + } + + @objc dynamic var recognizeShortenedNumbers: Bool { + get { bool(for: .recognizeShortenedNumbers) } + set { set(value: newValue, for: .recognizeShortenedNumbers) } + } + + @objc dynamic var defaultRegion: String { + get { string(for: .defaultRegion) } + set { set(value: newValue, for: .defaultRegion) } + } + + @objc dynamic var recordBeep: Bool { + get { bool(for: .recordBeep) } + set { set(value: newValue, for: .recordBeep) } + } + + @objc dynamic var showDebugInfo: Bool { + get { bool(for: .showDebugInfo) } + set { set(value: newValue, for: .showDebugInfo) } + } + + @objc dynamic var useTestBackend: Bool { + get { bool(for: .useTestBackend) } + set { set(value: newValue, for: .useTestBackend) } + } + + @objc dynamic var user: Data? { + get { data(for: .user) } + set { set(value: newValue, for: .user) } + } +} diff --git a/AutoCatCore/Utils/Constants.swift b/AutoCatCore/Utils/Constants.swift index 491d5ad..056869d 100644 --- a/AutoCatCore/Utils/Constants.swift +++ b/AutoCatCore/Utils/Constants.swift @@ -11,6 +11,8 @@ public enum Constants { #endif } + public static var baseUrlDebug = "http://192.168.1.2:3000/" + public static let pnLettersMap: [Character: Character] = [ "А": "A", "В": "B", "Е": "E", "К": "K", "М": "M", "Н": "H", "О": "O", "Р": "P", "С": "C", "Т": "T", "У": "Y", "Х": "X" ] diff --git a/AutoCatCoreTests/SettingsServiceTests.swift b/AutoCatCoreTests/SettingsServiceTests.swift new file mode 100644 index 0000000..d7eaf31 --- /dev/null +++ b/AutoCatCoreTests/SettingsServiceTests.swift @@ -0,0 +1,84 @@ +// +// SettingsServiceTests.swift +// AutoCatCoreTests +// +// Created by Selim Mustafaev on 17.08.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import Testing +@testable import AutoCatCore + +extension Encodable { + + var data: Data? { + let encoder = JSONEncoder() + return try? encoder.encode(self) + } +} + +struct SettingsServiceTests { + + let testRegion = "xxx" + let testEmail = "test@test.test" + let testToken = "testToken" + let testUser: User + let dbName = "testUserDefaults" + + let defaults: UserDefaults + var settingsService: SettingsService + + init() { + self.testUser = User(email: testEmail, token: testToken) + self.defaults = UserDefaults(suiteName: dbName)! + self.defaults.removePersistentDomain(forName: dbName) + self.settingsService = SettingsService(defaults: defaults) + } + + @Test("Creating default user") + mutating func defaultUser() async throws { + + #expect(settingsService.user.email == "") + #expect(settingsService.user.token == "") + } + + @Test("Loading saved user") + func loadSavedUser() async throws { + + let data = try #require(testUser.data) + + defaults.setPersistentDomain(["user": data], forName: dbName) + let service = SettingsService(defaults: defaults) + + #expect(service.user.email == testEmail) + #expect(service.user.token == testToken) + } + + @Test("Save user") + func saveUser() async throws { + + settingsService.user = testUser + + let userData = defaults.data(forKey: "user") + #expect(userData == testUser.data) + } + + @Test("Save settings", .serialized, arguments: [true, false]) + func saveSettings(value: Bool) { + + settingsService.recognizeAlternativeOrder = value + settingsService.recognizeShortenedNumbers = value + settingsService.defaultRegion = testRegion + settingsService.recordBeep = value + settingsService.showDebugInfo = value + settingsService.useTestBackend = value + + #expect(defaults.bool(forKey: SettingsKey.recognizeAlternativeOrder.rawValue) == value) + #expect(defaults.bool(forKey: SettingsKey.recognizeShortenedNumbers.rawValue) == value) + #expect(defaults.string(forKey: SettingsKey.defaultRegion.rawValue) == testRegion) + #expect(defaults.bool(forKey: SettingsKey.recordBeep.rawValue) == value) + #expect(defaults.bool(forKey: SettingsKey.showDebugInfo.rawValue) == value) + #expect(defaults.bool(forKey: SettingsKey.useTestBackend.rawValue) == value) + } +}