Implementing vehicle actions (check, update, remove) for history screen

This commit is contained in:
Selim Mustafaev 2025-02-09 14:37:56 +03:00
parent 1dc5995031
commit ded951a880
13 changed files with 145 additions and 19 deletions

View File

@ -151,6 +151,7 @@
7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */; }; 7AB4E4382D3D0C5C0006D052 /* VehiclesArchive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */; };
7AB4E43B2D3D3F4F0006D052 /* VehicleServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E43A2D3D3F4F0006D052 /* VehicleServiceProtocol.swift */; }; 7AB4E43B2D3D3F4F0006D052 /* VehicleServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E43A2D3D3F4F0006D052 /* VehicleServiceProtocol.swift */; };
7AB4E43D2D3D3F7A0006D052 /* VehicleService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4E43C2D3D3F7A0006D052 /* VehicleService.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 */; }; 7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */; };
7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; }; 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; };
7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.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 = "<group>"; }; 7AB4E4372D3D0C5C0006D052 /* VehiclesArchive.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclesArchive.swift; sourceTree = "<group>"; };
7AB4E43A2D3D3F4F0006D052 /* VehicleServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleServiceProtocol.swift; sourceTree = "<group>"; }; 7AB4E43A2D3D3F4F0006D052 /* VehicleServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleServiceProtocol.swift; sourceTree = "<group>"; };
7AB4E43C2D3D3F7A0006D052 /* VehicleService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleService.swift; sourceTree = "<group>"; }; 7AB4E43C2D3D3F7A0006D052 /* VehicleService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleService.swift; sourceTree = "<group>"; };
7AB4E4652D58A16C0006D052 /* GenericError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GenericError.swift; sourceTree = "<group>"; };
7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = "<group>"; }; 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = "<group>"; };
7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 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 = "<group>"; }; 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceProtocol.swift; sourceTree = "<group>"; };
@ -661,6 +663,7 @@
7A3E30F22C18840600567704 /* ActivityItemSource.swift */, 7A3E30F22C18840600567704 /* ActivityItemSource.swift */,
7A14416D2C297F7C00E79018 /* Coordinator.swift */, 7A14416D2C297F7C00E79018 /* Coordinator.swift */,
7A14416F2C2998B200E79018 /* Formatters.swift */, 7A14416F2C2998B200E79018 /* Formatters.swift */,
7AB4E4652D58A16C0006D052 /* GenericError.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1390,6 +1393,7 @@
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */, 7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */,
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */, 7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */,
7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */, 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */,
7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */,
7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */,
7A71EF572D0A26B200943129 /* EventModel.swift in Sources */, 7A71EF572D0A26B200943129 /* EventModel.swift in Sources */,
7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */, 7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */,

View File

@ -126,14 +126,14 @@ class EventsViewModel: ACHudContainer {
if vehicle.unrecognized { if vehicle.unrecognized {
await wrapWithToast(showProgress: false) { [weak self] in await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { return } guard let self else { throw GenericError.somethingWentWrong }
vehicle = try await storageOperation() vehicle = try await storageOperation()
} }
return return
} }
await wrapWithToast { [weak self] in await wrapWithToast { [weak self] in
guard let self else { return } guard let self else { throw GenericError.somethingWentWrong }
let vehicle = try await apiOperation() let vehicle = try await apiOperation()
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
self.vehicle = vehicle self.vehicle = vehicle

View File

@ -24,6 +24,12 @@ struct HistoryScreen: View {
.onTapGesture { .onTapGesture {
Task { await viewModel.openReport(vehicle: vehicle) } 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 { //#Preview {

View File

@ -108,9 +108,39 @@ final class HistoryViewModel: ACHudContainer {
} }
} }
func checkNewNumber(_ number: String) async { func checkVehicle(number: String, isUpdate: Bool) async {
await wrapWithToast { [weak self] in do {
try await self?.vehicleService.check(number: number) 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)
} }
} }

View File

@ -65,14 +65,14 @@ class NotesViewModel: ACHudContainer {
if vehicle.unrecognized { if vehicle.unrecognized {
await wrapWithToast(showProgress: false) { [weak self] in await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { return } guard let self else { throw GenericError.somethingWentWrong }
vehicle = try await storageOp() vehicle = try await storageOp()
} }
return return
} }
await wrapWithToast { [weak self] in await wrapWithToast { [weak self] in
guard let self else { return } guard let self else { throw GenericError.somethingWentWrong }
let vehicle = try await apiOp() let vehicle = try await apiOp()
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
self.vehicle = vehicle self.vehicle = vehicle

View File

@ -91,9 +91,10 @@ class ReportViewModel: ACHudContainer {
} }
func checkGB() async { func checkGB() async {
await wrapWithToast { await wrapWithToast { [weak self] in
self.vehicle = try await self.apiService.checkVehicleGb(by: self.vehicle.getNumber()) guard let self else { throw GenericError.somethingWentWrong }
try await self.storageService.updateVehicle(dto: self.vehicle, policy: .ifExists) vehicle = try await apiService.checkVehicleGb(by: vehicle.getNumber())
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
} }
} }

View File

@ -24,7 +24,7 @@ extension ACHudContainer where Self: AnyObject {
try await closure() try await closure()
if showProgress { if showProgress && hud == .progress {
hud = nil hud = nil
} }
} catch { } catch {
@ -51,4 +51,16 @@ extension ACHudContainer where Self: AnyObject {
func showError(_ error: Error) { func showError(_ error: Error) {
hud = .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)
}
} }

View File

@ -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: "")
}
}
}

View File

@ -417,3 +417,5 @@
"Not updated" = "Не обновленные"; "Not updated" = "Не обновленные";
"Open in Maps" = "Открыть на карте"; "Open in Maps" = "Открыть на карте";
"Something went wrong" = "Что-то пошло не так";

View File

@ -79,4 +79,14 @@ public actor StorageService: StorageServiceProtocol {
.sorted(byKeyPath: "updatedDate", ascending: false) .sorted(byKeyPath: "updatedDate", ascending: false)
.map(\.shallowDto) .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)
}
}
} }

View File

@ -20,6 +20,7 @@ public protocol StorageServiceProtocol: Sendable {
func loadVehicle(number: String) async throws -> VehicleDto func loadVehicle(number: String) async throws -> VehicleDto
@discardableResult @discardableResult
func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool func updateVehicle(dto: VehicleDto, policy: DbUpdatePolicy) async throws -> Bool
func deleteVehicle(number: String) async throws
// Notes // Notes
func addNote(text: String, to number: String) async throws -> VehicleDto func addNote(text: String, to number: String) async throws -> VehicleDto

View File

@ -8,11 +8,10 @@
import Foundation import Foundation
public struct VehicleWithErrors: Sendable { public typealias VehicleWithErrors = (
vehicle: VehicleDto,
public var vehicle: VehicleDto errors: [Error]
public var errors: [Error] )
}
public final class VehicleService { public final class VehicleService {

View File

@ -23,16 +23,18 @@ struct StorageServiceTests {
let settingsServiceMock: MockSettingsServiceProtocol let settingsServiceMock: MockSettingsServiceProtocol
let storageService: StorageService let storageService: StorageService
let realmConfig: Realm.Configuration
init() async throws { init() async throws {
var config: Realm.Configuration = .defaultConfiguration var config = Realm.Configuration.defaultConfiguration
config.inMemoryIdentifier = UUID().uuidString config.inMemoryIdentifier = UUID().uuidString
self.realmConfig = config
settingsServiceMock = MockSettingsServiceProtocol() 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) given(settingsServiceMock)
.user.willReturn(User()) .user.willReturn(User())
@ -50,6 +52,10 @@ struct StorageServiceTests {
} }
} }
func makeRealm() throws -> Realm {
try Realm(configuration: realmConfig)
}
@Test("Load existing vehicle") @Test("Load existing vehicle")
func loadExistingVehicle() async throws { func loadExistingVehicle() async throws {
@ -89,4 +95,23 @@ struct StorageServiceTests {
#expect(updated == false) #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)
}
}
} }