Implementing vehicle actions (check, update, remove) for history screen
This commit is contained in:
parent
1dc5995031
commit
ded951a880
@ -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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -661,6 +663,7 @@
|
||||
7A3E30F22C18840600567704 /* ActivityItemSource.swift */,
|
||||
7A14416D2C297F7C00E79018 /* Coordinator.swift */,
|
||||
7A14416F2C2998B200E79018 /* Formatters.swift */,
|
||||
7AB4E4652D58A16C0006D052 /* GenericError.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -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 */,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
20
AutoCat/Utils/GenericError.swift
Normal file
20
AutoCat/Utils/GenericError.swift
Normal 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: "")
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -417,3 +417,5 @@
|
||||
"Not updated" = "Не обновленные";
|
||||
|
||||
"Open in Maps" = "Открыть на карте";
|
||||
|
||||
"Something went wrong" = "Что-то пошло не так";
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user