diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 230c541..8be1df7 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -151,6 +151,7 @@ 7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */; }; 7AB4E43B2D3D3F4F0006D052 /* VehicleServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E43A2D3D3F4F0006D052 /* VehicleServiceProtocol.swift */; }; 7AB4E43D2D3D3F7A0006D052 /* VehicleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E43C2D3D3F7A0006D052 /* VehicleService.swift */; }; + 7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E4652D58A16C0006D052 /* GenericError.swift */; }; 7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */; }; 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; }; 7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */; }; @@ -427,6 +428,7 @@ 7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclesArchive.swift; sourceTree = ""; }; 7AB4E43A2D3D3F4F0006D052 /* VehicleServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleServiceProtocol.swift; sourceTree = ""; }; 7AB4E43C2D3D3F7A0006D052 /* VehicleService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleService.swift; sourceTree = ""; }; + 7AB4E4652D58A16C0006D052 /* GenericError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericError.swift; sourceTree = ""; }; 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = ""; }; 7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceProtocol.swift; sourceTree = ""; }; @@ -661,6 +663,7 @@ 7A3E30F22C18840600567704 /* ActivityItemSource.swift */, 7A14416D2C297F7C00E79018 /* Coordinator.swift */, 7A14416F2C2998B200E79018 /* Formatters.swift */, + 7AB4E4652D58A16C0006D052 /* GenericError.swift */, ); path = Utils; sourceTree = ""; @@ -1390,6 +1393,7 @@ 7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */, 7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */, 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */, + 7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */, 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, 7A71EF572D0A26B200943129 /* EventModel.swift in Sources */, 7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */, diff --git a/AutoCat/Screens/EventsScreen/EventsViewModel.swift b/AutoCat/Screens/EventsScreen/EventsViewModel.swift index 040e5f9..8f680bc 100644 --- a/AutoCat/Screens/EventsScreen/EventsViewModel.swift +++ b/AutoCat/Screens/EventsScreen/EventsViewModel.swift @@ -126,14 +126,14 @@ class EventsViewModel: ACHudContainer { if vehicle.unrecognized { await wrapWithToast(showProgress: false) { [weak self] in - guard let self else { return } + guard let self else { throw GenericError.somethingWentWrong } vehicle = try await storageOperation() } return } await wrapWithToast { [weak self] in - guard let self else { return } + guard let self else { throw GenericError.somethingWentWrong } let vehicle = try await apiOperation() try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) self.vehicle = vehicle diff --git a/AutoCat/Screens/HistoryScreen/HistoryScreen.swift b/AutoCat/Screens/HistoryScreen/HistoryScreen.swift index 5f1c560..65af8f3 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryScreen.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryScreen.swift @@ -24,6 +24,12 @@ struct HistoryScreen: View { .onTapGesture { Task { await viewModel.openReport(vehicle: vehicle) } } + .swipeActions(allowsFullSwipe: false) { + makeActions(for: vehicle) + } + .contextMenu { + makeActions(for: vehicle, useLabels: true) + } } } } @@ -69,6 +75,22 @@ struct HistoryScreen: View { } } } + + @ViewBuilder + func makeActions(for vehicle: VehicleDto, useLabels: Bool = false) -> some View { + + Button { + Task { await viewModel.updateVehicle(vehicle) } + } label: { + Label(useLabels ? "Update" : "", systemImage: "arrow.2.circlepath") + } + + Button(role: .destructive) { + Task { await viewModel.deleteVehicle(vehicle) } + } label: { + Label(useLabels ? "Delete" : "", systemImage: "trash") + } + } } //#Preview { diff --git a/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift b/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift index 8b5e7eb..04a1690 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift @@ -108,9 +108,39 @@ final class HistoryViewModel: ACHudContainer { } } - func checkNewNumber(_ number: String) async { - await wrapWithToast { [weak self] in - try await self?.vehicleService.check(number: number) + func checkVehicle(number: String, isUpdate: Bool) async { + do { + hud = .progress + let (vehicle, errors) = isUpdate ? try await vehicleService.updateHistory(number: number) + : try await vehicleService.check(number: number) + await loadVehicles() + + if errors.isEmpty { + hud = nil + if !vehicle.unrecognized { + await openReport(vehicle: vehicle) + } + } else { + showErrors(errors) + } + } catch { + hud = .error(error) } } + + func checkNewNumber(_ number: String) async { + await checkVehicle(number: number, isUpdate: false) + } + + func deleteVehicle(_ vehicle: VehicleDto) async { + await wrapWithToast(showProgress: false) { [weak self] in + guard let self else { throw GenericError.somethingWentWrong } + try await storageService.deleteVehicle(number: vehicle.getNumber()) + await loadVehicles() + } + } + + func updateVehicle(_ vehicle: VehicleDto) async { + await checkVehicle(number: vehicle.getNumber(), isUpdate: true) + } } diff --git a/AutoCat/Screens/NotesScreen/NotesViewModel.swift b/AutoCat/Screens/NotesScreen/NotesViewModel.swift index 3ad0bd1..b206829 100644 --- a/AutoCat/Screens/NotesScreen/NotesViewModel.swift +++ b/AutoCat/Screens/NotesScreen/NotesViewModel.swift @@ -65,14 +65,14 @@ class NotesViewModel: ACHudContainer { if vehicle.unrecognized { await wrapWithToast(showProgress: false) { [weak self] in - guard let self else { return } + guard let self else { throw GenericError.somethingWentWrong } vehicle = try await storageOp() } return } await wrapWithToast { [weak self] in - guard let self else { return } + guard let self else { throw GenericError.somethingWentWrong } let vehicle = try await apiOp() try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) self.vehicle = vehicle diff --git a/AutoCat/Screens/ReportScreen/ReportViewModel.swift b/AutoCat/Screens/ReportScreen/ReportViewModel.swift index a70d4d6..dd00832 100644 --- a/AutoCat/Screens/ReportScreen/ReportViewModel.swift +++ b/AutoCat/Screens/ReportScreen/ReportViewModel.swift @@ -91,9 +91,10 @@ class ReportViewModel: ACHudContainer { } func checkGB() async { - await wrapWithToast { - self.vehicle = try await self.apiService.checkVehicleGb(by: self.vehicle.getNumber()) - try await self.storageService.updateVehicle(dto: self.vehicle, policy: .ifExists) + await wrapWithToast { [weak self] in + guard let self else { throw GenericError.somethingWentWrong } + vehicle = try await apiService.checkVehicleGb(by: vehicle.getNumber()) + try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) } } diff --git a/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift b/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift index 9f78ac1..e886d12 100644 --- a/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift +++ b/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift @@ -24,7 +24,7 @@ extension ACHudContainer where Self: AnyObject { try await closure() - if showProgress { + if showProgress && hud == .progress { hud = nil } } catch { @@ -51,4 +51,16 @@ extension ACHudContainer where Self: AnyObject { func showError(_ error: Error) { hud = .error(error) } + + func showErrors(_ errors: [Error]) { + guard !errors.isEmpty else { + return + } + + let errorText = errors + .map { $0.localizedDescription } + .joined(separator: "\n\n") + + hud = .message(errorText, .error) + } } diff --git a/AutoCat/Utils/GenericError.swift b/AutoCat/Utils/GenericError.swift new file mode 100644 index 0000000..a72dc95 --- /dev/null +++ b/AutoCat/Utils/GenericError.swift @@ -0,0 +1,20 @@ +// +// GenericError.swift +// AutoCat +// +// Created by Selim Mustafaev on 09.02.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import Foundation + +enum GenericError: LocalizedError { + + case somethingWentWrong + + var errorDescription: String? { + switch self { + case .somethingWentWrong: NSLocalizedString("Something went wrong", comment: "") + } + } +} diff --git a/AutoCat/ru.lproj/Localizable.strings b/AutoCat/ru.lproj/Localizable.strings index 8cf4748..8143a19 100644 --- a/AutoCat/ru.lproj/Localizable.strings +++ b/AutoCat/ru.lproj/Localizable.strings @@ -417,3 +417,5 @@ "Not updated" = "Не обновленные"; "Open in Maps" = "Открыть на карте"; + +"Something went wrong" = "Что-то пошло не так"; diff --git a/AutoCatCore/Services/StorageService/StorageService.swift b/AutoCatCore/Services/StorageService/StorageService.swift index 4c55d5a..aebaa24 100644 --- a/AutoCatCore/Services/StorageService/StorageService.swift +++ b/AutoCatCore/Services/StorageService/StorageService.swift @@ -79,4 +79,14 @@ public actor StorageService: StorageServiceProtocol { .sorted(byKeyPath: "updatedDate", ascending: false) .map(\.shallowDto) } + + public func deleteVehicle(number: String) async throws { + guard let vehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) else { + throw StorageError.vehicleNotFound + } + + try await realm.asyncWrite { + realm.delete(vehicle) + } + } } diff --git a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift index 905f189..c8c6779 100644 --- a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift +++ b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift @@ -20,6 +20,7 @@ public protocol StorageServiceProtocol: Sendable { func loadVehicle(number: String) async throws -> VehicleDto @discardableResult func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool + func deleteVehicle(number: String) async throws // Notes func addNote(text: String, to number: String) async throws -> VehicleDto diff --git a/AutoCatCore/Services/VehicleService/VehicleService.swift b/AutoCatCore/Services/VehicleService/VehicleService.swift index 1e7b764..1deb558 100644 --- a/AutoCatCore/Services/VehicleService/VehicleService.swift +++ b/AutoCatCore/Services/VehicleService/VehicleService.swift @@ -8,11 +8,10 @@ import Foundation -public struct VehicleWithErrors: Sendable { - - public var vehicle: VehicleDto - public var errors: [Error] -} +public typealias VehicleWithErrors = ( + vehicle: VehicleDto, + errors: [Error] +) public final class VehicleService { diff --git a/AutoCatCoreTests/Storage/StorageServiceTests.swift b/AutoCatCoreTests/Storage/StorageServiceTests.swift index d757cf1..b63fe4c 100644 --- a/AutoCatCoreTests/Storage/StorageServiceTests.swift +++ b/AutoCatCoreTests/Storage/StorageServiceTests.swift @@ -23,16 +23,18 @@ struct StorageServiceTests { let settingsServiceMock: MockSettingsServiceProtocol let storageService: StorageService + let realmConfig: Realm.Configuration init() async throws { - var config: Realm.Configuration = .defaultConfiguration + var config = Realm.Configuration.defaultConfiguration config.inMemoryIdentifier = UUID().uuidString + self.realmConfig = config settingsServiceMock = MockSettingsServiceProtocol() - self.storageService = try await StorageService(settingsService: settingsServiceMock, config: config) + self.storageService = try await StorageService(settingsService: settingsServiceMock, config: realmConfig) - try addTestVehicle(config: config) + try addTestVehicle(config: realmConfig) given(settingsServiceMock) .user.willReturn(User()) @@ -50,6 +52,10 @@ struct StorageServiceTests { } } + func makeRealm() throws -> Realm { + try Realm(configuration: realmConfig) + } + @Test("Load existing vehicle") func loadExistingVehicle() async throws { @@ -89,4 +95,23 @@ struct StorageServiceTests { #expect(updated == false) } } + + @Test("Delete vehicle") + func deleteVehicle() async throws { + + try await storageService.deleteVehicle(number: existingVehicleNumber) + + let realm = try makeRealm() + let vehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: existingVehicleNumber) + + #expect(vehicle == nil) + } + + @Test("Delete non-existent vehicle") + func deleteNonExistentVehicle() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + try await storageService.deleteVehicle(number: nonExistingVehicleNumber) + } + } }