From 81bdf64b618e1b381063a53b11c4e5519030b661 Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Mon, 5 May 2025 13:51:50 +0300 Subject: [PATCH] Fixed error on location picker screen --- AutoCat.xcodeproj/project.pbxproj | 4 +- .../EventsScreen/EventsViewModel.swift | 16 +++--- .../FiltersScreen/FiltersViewModel.swift | 6 +-- .../LocationPickerScreen.swift | 3 +- .../LocationPickerViewModel.swift | 32 ++++++++--- .../Screens/NotesScreen/NotesViewModel.swift | 8 +-- .../ReportScreen/ReportViewModel.swift | 20 +++---- AutoCatTests/HistoryTests.swift | 14 ++--- AutoCatTests/LocationPickerTests.swift | 53 +++++++++++++------ 9 files changed, 98 insertions(+), 58 deletions(-) diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 6ce9b9f..0461920 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -1657,7 +1657,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 160; + CURRENT_PROJECT_VERSION = 161; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; @@ -1686,7 +1686,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 160; + CURRENT_PROJECT_VERSION = 161; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; diff --git a/AutoCat/Screens/EventsScreen/EventsViewModel.swift b/AutoCat/Screens/EventsScreen/EventsViewModel.swift index 5b4c749..b91490c 100644 --- a/AutoCat/Screens/EventsScreen/EventsViewModel.swift +++ b/AutoCat/Screens/EventsScreen/EventsViewModel.swift @@ -39,7 +39,7 @@ class EventsViewModel: ACHudContainer { var showPasteAlert: Bool = false var pastedEvent: VehicleEventDto? - var onUpdate: (VehicleDto) -> Void + var onUpdate: ((VehicleDto) -> Void)? var isPasteAvailable: Bool { #if os(macOS) @@ -54,11 +54,13 @@ class EventsViewModel: ACHudContainer { settingsService.user.hasPermission(.locationAuthor) } - init(apiService: ApiServiceProtocol, - storageService: StorageServiceProtocol, - settingsService: SettingsServiceProtocol, - vehicle: VehicleDto, - onUpdate: @escaping (VehicleDto) -> Void) { + init( + apiService: ApiServiceProtocol, + storageService: StorageServiceProtocol, + settingsService: SettingsServiceProtocol, + vehicle: VehicleDto, + onUpdate: ((VehicleDto) -> Void)? = nil + ) { self.apiService = apiService self.storageService = storageService @@ -93,7 +95,7 @@ class EventsViewModel: ACHudContainer { } if !initialLoad { - onUpdate(vehicle) + onUpdate?(vehicle) } } diff --git a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift index 8e67412..0f24dae 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift @@ -33,12 +33,12 @@ class FiltersViewModel { @ObservationIgnored var currentBrand: StringOption = .any - let onUpdate: (Filter) -> Void + let onUpdate: ((Filter) -> Void)? init( apiService: ApiServiceProtocol, filter: Filter, - onUpdate: @escaping (Filter) -> Void + onUpdate: ((Filter) -> Void)? = nil ) { self.apiService = apiService self.filter = filter @@ -65,7 +65,7 @@ class FiltersViewModel { } func applyFilters() { - onUpdate(filter) + onUpdate?(filter) } func nullifyTime(of date: Date?) -> Date? { diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift index b3599c4..bb2f587 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift @@ -27,7 +27,7 @@ struct LocationPickerScreen: View { var body: some View { ZStack { - Map(initialPosition: viewModel.position) + Map(position: $viewModel.position) .mapControls { MapUserLocationButton() } @@ -43,6 +43,7 @@ struct LocationPickerScreen: View { .foregroundColor(.blue) } //.ignoresSafeArea() + .onAppear(perform: viewModel.onAppear) .navigationTitle(viewModel.event.location) .toolbar { ToolbarItem(placement: .primaryAction) { diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift index 21794cc..6b5be70 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift @@ -18,19 +18,26 @@ final class LocationPickerViewModel { let locationService: LocationServiceProtocol var event: VehicleEventDto - var position: MapCameraPosition + var position: MapCameraPosition = .automatic - var onUpdate: (VehicleEventDto) -> Void + var onUpdate: ((VehicleEventDto) -> Void)? + + @ObservationIgnored + @AutoCancellable + var geocoderTask: Task? init( locationService: LocationServiceProtocol, event: VehicleEventDto, - onUpdate: @escaping (VehicleEventDto) -> Void + onUpdate: ((VehicleEventDto) -> Void)? = nil ) { self.locationService = locationService self.event = event self.onUpdate = onUpdate + } + + func onAppear() { if event.latitude == 0 && event.longitude == 0 { self.position = .userLocation(fallback: .automatic) @@ -43,11 +50,24 @@ final class LocationPickerViewModel { func updateEvent(center: CLLocationCoordinate2D) async { event.latitude = center.latitude event.longitude = center.longitude - event.address = try? await locationService.getAddressForLocation(latitude: center.latitude, - longitude: center.longitude) + + geocoderTask = Task { + try? await Task.sleep(for: .milliseconds(200)) + + if Task.isCancelled { + return + } + + event.address = try? await locationService.getAddressForLocation( + latitude: center.latitude, + longitude: center.longitude + ) + } + + _ = await geocoderTask?.result } func done() { - onUpdate(event) + onUpdate?(event) } } diff --git a/AutoCat/Screens/NotesScreen/NotesViewModel.swift b/AutoCat/Screens/NotesScreen/NotesViewModel.swift index f59f0ed..1949216 100644 --- a/AutoCat/Screens/NotesScreen/NotesViewModel.swift +++ b/AutoCat/Screens/NotesScreen/NotesViewModel.swift @@ -28,13 +28,13 @@ class NotesViewModel: ACHudContainer { var vehicle: VehicleDto var hud: ACHud? - var onUpdate: (VehicleDto) -> Void + var onUpdate: ((VehicleDto) -> Void)? init( storageService: StorageServiceProtocol, apiService: ApiServiceProtocol, vehicle: VehicleDto, - onUpdate: @escaping (VehicleDto) -> Void + onUpdate: ((VehicleDto) -> Void)? = nil ) { self.storageService = storageService @@ -80,7 +80,7 @@ class NotesViewModel: ACHudContainer { await wrapWithToast(showProgress: false) { [weak self] in guard let self else { throw GenericError.somethingWentWrong } vehicle = try await storageOp() - onUpdate(vehicle) + onUpdate?(vehicle) } return } @@ -90,7 +90,7 @@ class NotesViewModel: ACHudContainer { let vehicle = try await apiOp() try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) self.vehicle = vehicle - onUpdate(vehicle) + onUpdate?(vehicle) } } diff --git a/AutoCat/Screens/ReportScreen/ReportViewModel.swift b/AutoCat/Screens/ReportScreen/ReportViewModel.swift index 13d3a55..7b412da 100644 --- a/AutoCat/Screens/ReportScreen/ReportViewModel.swift +++ b/AutoCat/Screens/ReportScreen/ReportViewModel.swift @@ -21,7 +21,7 @@ class ReportViewModel: ACHudContainer { var vehicle: VehicleDto var hud: ACHud? - let onUpdate: (VehicleDto) -> Void + let onUpdate: ((VehicleDto) -> Void)? var plateNumber: String { if vehicle.outdated, let current = vehicle.currentNumber { @@ -55,12 +55,14 @@ class ReportViewModel: ACHudContainer { return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt) } - init(apiService: ApiServiceProtocol, - storageService: StorageServiceProtocol, - settingsService: SettingsServiceProtocol, - vehicle: VehicleDto, - isPersistent: Bool, - onUpdate: @escaping (VehicleDto) -> Void) { + init( + apiService: ApiServiceProtocol, + storageService: StorageServiceProtocol, + settingsService: SettingsServiceProtocol, + vehicle: VehicleDto, + isPersistent: Bool, + onUpdate: ((VehicleDto) -> Void)? = nil + ) { self.apiService = apiService self.storageService = storageService @@ -97,12 +99,12 @@ class ReportViewModel: ACHudContainer { guard let self else { throw GenericError.somethingWentWrong } vehicle = try await apiService.checkVehicleGb(by: vehicle.getNumber()) try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) - onUpdate(vehicle) + onUpdate?(vehicle) } } func onVehicleChanged(_ vehicle: VehicleDto) { self.vehicle = vehicle - onUpdate(vehicle) + onUpdate?(vehicle) } } diff --git a/AutoCatTests/HistoryTests.swift b/AutoCatTests/HistoryTests.swift index 51126ac..5b6ab8a 100644 --- a/AutoCatTests/HistoryTests.swift +++ b/AutoCatTests/HistoryTests.swift @@ -140,8 +140,8 @@ struct HistoryTests { let updatedVehicle: VehicleDto = .normal.addNote(text: "123") given(storageServiceMock) - .loadVehicles() - .willReturn([.normal]) + .loadVehicles().willReturn([.normal]) + .loadVehicles().willReturn([updatedVehicle]) given(storageServiceMock) .dbFileURL @@ -151,14 +151,6 @@ struct HistoryTests { .updateHistory(number: .any) .willReturn((vehicle: updatedVehicle, errors: [])) - when(storageServiceMock) - .loadVehicles() - .perform { - given(storageServiceMock) - .loadVehicles() - .willReturn([updatedVehicle]) - } - await viewModel.onAppear() await viewModel.updateVehicle(.normal) @@ -168,7 +160,7 @@ struct HistoryTests { verify(storageServiceMock) .loadVehicles() - .called(.exactly(3)) + .called(.exactly(2)) #expect(viewModel.vehicles.count == 1) #expect(viewModel.vehiclesFiltered.count == 1) diff --git a/AutoCatTests/LocationPickerTests.swift b/AutoCatTests/LocationPickerTests.swift index f5eb4d0..215ebcf 100644 --- a/AutoCatTests/LocationPickerTests.swift +++ b/AutoCatTests/LocationPickerTests.swift @@ -22,32 +22,30 @@ struct LocationPickerTests { let longitude: CLLocationDegrees = 10 let address = "Test Address" - let geocoderMock = MockGeocoderProtocol() + let locationServiceMock = MockLocationServiceProtocol() func makeViewModel(event: VehicleEventDto) -> LocationPickerViewModel { - let locationService = LocationService( - geocoder: geocoderMock, - locationManager: MockSwiftLocationProtocol(), - settingsService: MockSettingsServiceProtocol() + return LocationPickerViewModel( + locationService: locationServiceMock, + event: event ) - - return LocationPickerViewModel(locationService: locationService, - event: event) } @Test("Set initial location (user)") func setInitialLocationUser() async throws { let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) + viewModel.onAppear() #expect(viewModel.position == .userLocation(fallback: .automatic)) } - + @Test("Set initial location (custom)") func setInitialLocationCustom() async throws { let viewModel = makeViewModel(event: .init(lat: latitude, lon: longitude, addedBy: nil)) + viewModel.onAppear() #expect(viewModel.position.region?.center.latitude == latitude) #expect(viewModel.position.region?.center.longitude == longitude) @@ -58,12 +56,9 @@ struct LocationPickerTests { let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) - let location = CLLocation(latitude: latitude, longitude: longitude) - let placemark = CLPlacemark(location: location, name: address, postalAddress: nil) - - given(geocoderMock) - .reverseGeocodeLocation(.any) - .willReturn([placemark]) + given(locationServiceMock) + .getAddressForLocation(latitude: .value(latitude), longitude: .value(longitude)) + .willReturn(address) await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude)) @@ -71,4 +66,32 @@ struct LocationPickerTests { #expect(viewModel.event.longitude == longitude) #expect(viewModel.event.address == address) } + + @Test("Update event (throttling)") + func updateEventThrottling() async throws { + + let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) + + let initialLocation = CLLocationCoordinate2D(latitude: 0, longitude: 0) + let finalLocation = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + + given(locationServiceMock) + .getAddressForLocation(latitude: .value(0), longitude: .value(0)).willReturn("") + .getAddressForLocation(latitude: .value(latitude), longitude: .value(longitude)).willReturn(address) + + viewModel.onAppear() + + async let task1: () = viewModel.updateEvent(center: initialLocation) + async let task2: () = viewModel.updateEvent(center: initialLocation) + async let task3: () = viewModel.updateEvent(center: finalLocation) + _ = await (task1, task2, task3) + + verify(locationServiceMock) + .getAddressForLocation(latitude: .value(0), longitude: .value(0)).called(.never) + .getAddressForLocation(latitude: .value(latitude), longitude: .value(longitude)).called(.once) + + #expect(viewModel.event.latitude == latitude) + #expect(viewModel.event.longitude == longitude) + #expect(viewModel.event.address == address) + } }