From 406b1b02b5837aac656c65869103c63a3d193f0a Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Sun, 6 Apr 2025 13:38:10 +0300 Subject: [PATCH] SwiftUI version of auth screen --- AutoCat.xcodeproj/project.pbxproj | 24 +++++++ AutoCat/SceneDelegate.swift | 4 +- .../Screens/AuthScreen/AuthCoordinator.swift | 43 +++++++++++ AutoCat/Screens/AuthScreen/AuthScreen.swift | 47 ++++++++++++ .../Screens/AuthScreen/AuthViewModel.swift | 72 +++++++++++++++++++ .../SettingsScreen/SettingsCoordinator.swift | 7 +- AutoCat/SwiftUI/ACButtonView.swift | 32 +++++++++ AutoCat/ru.lproj/Localizable.strings | 6 +- .../ApiService/ApiServiceProtocol.swift | 3 + .../StorageService/StorageService.swift | 7 ++ .../StorageServiceProtocol.swift | 2 + 11 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 AutoCat/Screens/AuthScreen/AuthCoordinator.swift create mode 100644 AutoCat/Screens/AuthScreen/AuthScreen.swift create mode 100644 AutoCat/Screens/AuthScreen/AuthViewModel.swift create mode 100644 AutoCat/SwiftUI/ACButtonView.swift diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 6916f47..b96dffd 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -194,6 +194,10 @@ 7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6CA02512244400F237B2 /* MapExt.swift */; }; 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE24C5E251F1B4E00758E39 /* Buttons.swift */; }; 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; }; + 7AF231932DA1C28100AE5EB3 /* AuthScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231922DA1C28100AE5EB3 /* AuthScreen.swift */; }; + 7AF231952DA1C29300AE5EB3 /* AuthViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231942DA1C29300AE5EB3 /* AuthViewModel.swift */; }; + 7AF231972DA1C30000AE5EB3 /* AuthCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231962DA1C30000AE5EB3 /* AuthCoordinator.swift */; }; + 7AF231992DA27C1B00AE5EB3 /* ACButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF231982DA27C1B00AE5EB3 /* ACButtonView.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 */; }; @@ -477,6 +481,10 @@ 7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; 7AE8424D26109F78002F6B31 /* Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exportable.swift; sourceTree = ""; }; + 7AF231922DA1C28100AE5EB3 /* AuthScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthScreen.swift; sourceTree = ""; }; + 7AF231942DA1C29300AE5EB3 /* AuthViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthViewModel.swift; sourceTree = ""; }; + 7AF231962DA1C30000AE5EB3 /* AuthCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthCoordinator.swift; sourceTree = ""; }; + 7AF231982DA27C1B00AE5EB3 /* ACButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACButtonView.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 = ""; }; @@ -718,6 +726,7 @@ 7A1441632C297E9800E79018 /* Screens */ = { isa = PBXGroup; children = ( + 7AF231912DA1C26C00AE5EB3 /* AuthScreen */, 7A95197E2D80B69800E69883 /* RecordsScreen */, 7A5911EC2D63225500EC51BA /* SearchScreen */, 7A131FD12D37B74100DC7755 /* HistoryScreen */, @@ -1116,6 +1125,16 @@ path = Extensions; sourceTree = ""; }; + 7AF231912DA1C26C00AE5EB3 /* AuthScreen */ = { + isa = PBXGroup; + children = ( + 7AF231922DA1C28100AE5EB3 /* AuthScreen.swift */, + 7AF231942DA1C29300AE5EB3 /* AuthViewModel.swift */, + 7AF231962DA1C30000AE5EB3 /* AuthCoordinator.swift */, + ); + path = AuthScreen; + sourceTree = ""; + }; 7AF6D1F02677C03B0086EA64 /* AutoCatCore */ = { isa = PBXGroup; children = ( @@ -1177,6 +1196,7 @@ 7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */, 7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */, 7A589E0E2D6B6E8E00EF3FBE /* NumberEditView.swift */, + 7AF231982DA27C1B00AE5EB3 /* ACButtonView.swift */, ); path = SwiftUI; sourceTree = ""; @@ -1446,6 +1466,7 @@ 7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */, 7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */, 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */, + 7AF231952DA1C29300AE5EB3 /* AuthViewModel.swift in Sources */, 7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */, 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, 7A71EF572D0A26B200943129 /* EventModel.swift in Sources */, @@ -1460,6 +1481,7 @@ 7AC76D7B270083AE0084DB27 /* TextView.swift in Sources */, 7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */, 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */, + 7AF231972DA1C30000AE5EB3 /* AuthCoordinator.swift in Sources */, 7A11471A23FE839000B424AF /* AuthController.swift in Sources */, 7A5911F22D63268400EC51BA /* SearchCoordinator.swift in Sources */, 7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */, @@ -1474,6 +1496,7 @@ 7ADF6C9F251201D200F237B2 /* GlobalEventsController.swift in Sources */, 7A1022792C557ED600B84627 /* LocationPickerViewModel.swift in Sources */, 7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */, + 7AF231932DA1C28100AE5EB3 /* AuthScreen.swift in Sources */, 7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */, 7A10226E2C551EE000B84627 /* LocationEditViewModel.swift in Sources */, 7AB4902B2D6B1446002F39C6 /* ACKeyboardButton.swift in Sources */, @@ -1491,6 +1514,7 @@ 7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */, 7A06E0AC2C7065AC005731AC /* SettingsScreen.swift in Sources */, 7A4322952CB2CD0F00085CF6 /* FiltersCoordinator.swift in Sources */, + 7AF231992DA27C1B00AE5EB3 /* ACButtonView.swift in Sources */, 7A131FD72D37B77E00DC7755 /* HistoryCoordinator.swift in Sources */, 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */, 7A4955822D58CCF900912E66 /* HistoryFilter.swift in Sources */, diff --git a/AutoCat/SceneDelegate.swift b/AutoCat/SceneDelegate.swift index 63d3935..5cfb715 100644 --- a/AutoCat/SceneDelegate.swift +++ b/AutoCat/SceneDelegate.swift @@ -84,7 +84,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let settingsService = ServiceContainer.shared.resolve(SettingsServiceProtocol.self) if settingsService.user.token.isEmpty { - self.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController") + let coordinator = AuthCoordinator(window: self.window) + self.window?.rootViewController = coordinator.start() + //self.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController") } else { self.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController") if let number { diff --git a/AutoCat/Screens/AuthScreen/AuthCoordinator.swift b/AutoCat/Screens/AuthScreen/AuthCoordinator.swift new file mode 100644 index 0000000..6ddc4d7 --- /dev/null +++ b/AutoCat/Screens/AuthScreen/AuthCoordinator.swift @@ -0,0 +1,43 @@ +// +// AuthCoordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 05.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import UIKit +import SwiftUI +import AutoCatCore + +@MainActor +final class AuthCoordinator { + + let window: UIWindow? + + init(window: UIWindow?) { + + self.window = window + } + + func start() -> UIViewController { + + let resolver = ServiceContainer.shared + let viewModel = AuthViewModel( + apiService: resolver.resolve(ApiServiceProtocol.self), + storageService: resolver.resolve(StorageServiceProtocol.self), + settingsService: resolver.resolve(SettingsServiceProtocol.self) + ) + + viewModel.coordinator = self + + let view = AuthScreen(viewModel: viewModel) + return UIHostingController(rootView: view) + } + + func openMainScreen() { + + let storyboard = UIStoryboard(name: "Main", bundle: nil) + window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController") + } +} diff --git a/AutoCat/Screens/AuthScreen/AuthScreen.swift b/AutoCat/Screens/AuthScreen/AuthScreen.swift new file mode 100644 index 0000000..7dd7213 --- /dev/null +++ b/AutoCat/Screens/AuthScreen/AuthScreen.swift @@ -0,0 +1,47 @@ +// +// AuthScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 05.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct AuthScreen: View { + + @State var viewModel: AuthViewModel + + var body: some View { + ZStack { + VStack(spacing: 16) { + Group { + TextField("Email", text: $viewModel.email, prompt: Text("Email")) + .keyboardType(.emailAddress) + SecureField("Password", text: $viewModel.password, prompt: Text("Password")) + } + .padding(8) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.background) + .stroke(.secondary) + } + + Group { + ACButtonView(title: "Log In") { + Task { await viewModel.login() } + } + ACButtonView(title: "Sign up") { + Task { await viewModel.signup() } + } + } + .disabled(!viewModel.actionsEnabled) + .opacity(viewModel.actionsEnabled ? 1 : 0.5) + } + .padding(.horizontal, 32) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .hud($viewModel.hud) + .onAppear(perform: viewModel.onAppear) + } +} diff --git a/AutoCat/Screens/AuthScreen/AuthViewModel.swift b/AutoCat/Screens/AuthScreen/AuthViewModel.swift new file mode 100644 index 0000000..9f4ecdb --- /dev/null +++ b/AutoCat/Screens/AuthScreen/AuthViewModel.swift @@ -0,0 +1,72 @@ +// +// AuthViewModel.swift +// AutoCat +// +// Created by Selim Mustafaev on 05.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +@MainActor +@Observable +final class AuthViewModel: ACHudContainer { + + let apiService: ApiServiceProtocol + let storageService: StorageServiceProtocol + var settingsService: SettingsServiceProtocol + var coordinator: AuthCoordinator? + + var hud: ACHud? + + var email: String = "" + var password: String = "" + + var actionsEnabled: Bool { + email.count >= 5 && password.count >= 5 + } + + init( + apiService: ApiServiceProtocol, + storageService: StorageServiceProtocol, + settingsService: SettingsServiceProtocol + ) { + self.apiService = apiService + self.storageService = storageService + self.settingsService = settingsService + } + + func onAppear() { + + if settingsService.user.email.count > 0 { + email = settingsService.user.email + } + } + + func login() async { + await wrapWithToast { [weak self] in + guard let self else { return } + let user = try await apiService.login(email: email, password: password) + try await saveUser(user) + coordinator?.openMainScreen() + } + } + + func signup() async { + await wrapWithToast { [weak self] in + guard let self else { return } + let user = try await apiService.signUp(email: email, password: password) + try await saveUser(user) + coordinator?.openMainScreen() + } + } + + func saveUser(_ user: User) async throws { + if user.email != settingsService.user.email { + try await storageService.deleteAll() + } + + settingsService.user = user + } +} diff --git a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift index e87eca2..4e6bd71 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift @@ -36,9 +36,12 @@ class SettingsCoordinator: Coordinator { } func openAuthScreen() { + guard let window = viewController?.tabBar.window else { + return + } - let storyboard = UIStoryboard(name: "Main", bundle: nil) - viewController?.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "AuthController") + let coordinator = AuthCoordinator(window: window) + window.rootViewController = coordinator.start() } func openGoogleOauthPage() { diff --git a/AutoCat/SwiftUI/ACButtonView.swift b/AutoCat/SwiftUI/ACButtonView.swift new file mode 100644 index 0000000..14ec877 --- /dev/null +++ b/AutoCat/SwiftUI/ACButtonView.swift @@ -0,0 +1,32 @@ +// +// ACButtonView.swift +// AutoCat +// +// Created by Selim Mustafaev on 06.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct ACButtonView: View { + + let title: LocalizedStringKey + let onTap: () -> Void + + var body: some View { + Button(action: onTap) { + Text(title) + .frame(maxWidth: .infinity) + .padding(12) + .foregroundStyle(.white) + .background { + RoundedRectangle(cornerRadius: 8) + .fill(.blue) + } + } + } +} + +#Preview { + ACButtonView(title: "Button") {} +} diff --git a/AutoCat/ru.lproj/Localizable.strings b/AutoCat/ru.lproj/Localizable.strings index 365c770..26a8f64 100644 --- a/AutoCat/ru.lproj/Localizable.strings +++ b/AutoCat/ru.lproj/Localizable.strings @@ -206,7 +206,7 @@ "Location adding time" = "Время добавления локаций"; /* No comment provided by engineer. */ -"Log In" = "Войти"; +"Log In" = "Вход"; /* No comment provided by engineer. */ "Main filters" = "Основные фильтры"; @@ -423,3 +423,7 @@ "Are you sure you want to delete this event?" = "Вы уверены, что хотите удалить это событие?"; "Voice records" = "Голосовые записи"; + +"Password" = "Пароль"; + +"Sign up" = "Регистрация"; diff --git a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift index 78853ae..4154f1f 100644 --- a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift +++ b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift @@ -11,6 +11,9 @@ import Mockable @Mockable public protocol ApiServiceProtocol: Sendable { + func login(email: String, password: String) async throws -> User + func signUp(email: String, password: String) async throws -> User + func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto func edit(note: VehicleNoteDto) async throws -> VehicleDto func remove(note id: String) async throws -> VehicleDto diff --git a/AutoCatCore/Services/StorageService/StorageService.swift b/AutoCatCore/Services/StorageService/StorageService.swift index 4106b84..d7c631c 100644 --- a/AutoCatCore/Services/StorageService/StorageService.swift +++ b/AutoCatCore/Services/StorageService/StorageService.swift @@ -28,6 +28,13 @@ public actor StorageService: StorageServiceProtocol { } } + public func deleteAll() async throws { + + try await realm.asyncWrite { + realm.deleteAll() + } + } + @discardableResult public func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool { diff --git a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift index 9f59884..729df4e 100644 --- a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift +++ b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift @@ -15,6 +15,8 @@ public protocol StorageServiceProtocol: Sendable { // Generic var dbFileURL: URL? { get async } + func deleteAll() async throws + // Vehicles func loadVehicles() async -> [VehicleDto] func loadVehicle(number: String) async throws -> VehicleDto