Fixed error on location picker screen

This commit is contained in:
Selim Mustafaev 2025-05-05 13:51:50 +03:00
parent 23224eb2bf
commit 81bdf64b61
9 changed files with 98 additions and 58 deletions

View File

@ -1657,7 +1657,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 160; CURRENT_PROJECT_VERSION = 161;
DEVELOPMENT_TEAM = 46DTTB8X4S; DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_FILE = AutoCat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoCat; INFOPLIST_KEY_CFBundleDisplayName = AutoCat;
@ -1686,7 +1686,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 160; CURRENT_PROJECT_VERSION = 161;
DEVELOPMENT_TEAM = 46DTTB8X4S; DEVELOPMENT_TEAM = 46DTTB8X4S;
INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_FILE = AutoCat/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AutoCat; INFOPLIST_KEY_CFBundleDisplayName = AutoCat;

View File

@ -39,7 +39,7 @@ class EventsViewModel: ACHudContainer {
var showPasteAlert: Bool = false var showPasteAlert: Bool = false
var pastedEvent: VehicleEventDto? var pastedEvent: VehicleEventDto?
var onUpdate: (VehicleDto) -> Void var onUpdate: ((VehicleDto) -> Void)?
var isPasteAvailable: Bool { var isPasteAvailable: Bool {
#if os(macOS) #if os(macOS)
@ -54,11 +54,13 @@ class EventsViewModel: ACHudContainer {
settingsService.user.hasPermission(.locationAuthor) settingsService.user.hasPermission(.locationAuthor)
} }
init(apiService: ApiServiceProtocol, init(
storageService: StorageServiceProtocol, apiService: ApiServiceProtocol,
settingsService: SettingsServiceProtocol, storageService: StorageServiceProtocol,
vehicle: VehicleDto, settingsService: SettingsServiceProtocol,
onUpdate: @escaping (VehicleDto) -> Void) { vehicle: VehicleDto,
onUpdate: ((VehicleDto) -> Void)? = nil
) {
self.apiService = apiService self.apiService = apiService
self.storageService = storageService self.storageService = storageService
@ -93,7 +95,7 @@ class EventsViewModel: ACHudContainer {
} }
if !initialLoad { if !initialLoad {
onUpdate(vehicle) onUpdate?(vehicle)
} }
} }

View File

@ -33,12 +33,12 @@ class FiltersViewModel {
@ObservationIgnored var currentBrand: StringOption = .any @ObservationIgnored var currentBrand: StringOption = .any
let onUpdate: (Filter) -> Void let onUpdate: ((Filter) -> Void)?
init( init(
apiService: ApiServiceProtocol, apiService: ApiServiceProtocol,
filter: Filter, filter: Filter,
onUpdate: @escaping (Filter) -> Void onUpdate: ((Filter) -> Void)? = nil
) { ) {
self.apiService = apiService self.apiService = apiService
self.filter = filter self.filter = filter
@ -65,7 +65,7 @@ class FiltersViewModel {
} }
func applyFilters() { func applyFilters() {
onUpdate(filter) onUpdate?(filter)
} }
func nullifyTime(of date: Date?) -> Date? { func nullifyTime(of date: Date?) -> Date? {

View File

@ -27,7 +27,7 @@ struct LocationPickerScreen: View {
var body: some View { var body: some View {
ZStack { ZStack {
Map(initialPosition: viewModel.position) Map(position: $viewModel.position)
.mapControls { .mapControls {
MapUserLocationButton() MapUserLocationButton()
} }
@ -43,6 +43,7 @@ struct LocationPickerScreen: View {
.foregroundColor(.blue) .foregroundColor(.blue)
} }
//.ignoresSafeArea() //.ignoresSafeArea()
.onAppear(perform: viewModel.onAppear)
.navigationTitle(viewModel.event.location) .navigationTitle(viewModel.event.location)
.toolbar { .toolbar {
ToolbarItem(placement: .primaryAction) { ToolbarItem(placement: .primaryAction) {

View File

@ -18,19 +18,26 @@ final class LocationPickerViewModel {
let locationService: LocationServiceProtocol let locationService: LocationServiceProtocol
var event: VehicleEventDto var event: VehicleEventDto
var position: MapCameraPosition var position: MapCameraPosition = .automatic
var onUpdate: (VehicleEventDto) -> Void var onUpdate: ((VehicleEventDto) -> Void)?
@ObservationIgnored
@AutoCancellable
var geocoderTask: Task<Void, Never>?
init( init(
locationService: LocationServiceProtocol, locationService: LocationServiceProtocol,
event: VehicleEventDto, event: VehicleEventDto,
onUpdate: @escaping (VehicleEventDto) -> Void onUpdate: ((VehicleEventDto) -> Void)? = nil
) { ) {
self.locationService = locationService self.locationService = locationService
self.event = event self.event = event
self.onUpdate = onUpdate self.onUpdate = onUpdate
}
func onAppear() {
if event.latitude == 0 && event.longitude == 0 { if event.latitude == 0 && event.longitude == 0 {
self.position = .userLocation(fallback: .automatic) self.position = .userLocation(fallback: .automatic)
@ -43,11 +50,24 @@ final class LocationPickerViewModel {
func updateEvent(center: CLLocationCoordinate2D) async { func updateEvent(center: CLLocationCoordinate2D) async {
event.latitude = center.latitude event.latitude = center.latitude
event.longitude = center.longitude 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() { func done() {
onUpdate(event) onUpdate?(event)
} }
} }

View File

@ -28,13 +28,13 @@ class NotesViewModel: ACHudContainer {
var vehicle: VehicleDto var vehicle: VehicleDto
var hud: ACHud? var hud: ACHud?
var onUpdate: (VehicleDto) -> Void var onUpdate: ((VehicleDto) -> Void)?
init( init(
storageService: StorageServiceProtocol, storageService: StorageServiceProtocol,
apiService: ApiServiceProtocol, apiService: ApiServiceProtocol,
vehicle: VehicleDto, vehicle: VehicleDto,
onUpdate: @escaping (VehicleDto) -> Void onUpdate: ((VehicleDto) -> Void)? = nil
) { ) {
self.storageService = storageService self.storageService = storageService
@ -80,7 +80,7 @@ class NotesViewModel: ACHudContainer {
await wrapWithToast(showProgress: false) { [weak self] in await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { throw GenericError.somethingWentWrong } guard let self else { throw GenericError.somethingWentWrong }
vehicle = try await storageOp() vehicle = try await storageOp()
onUpdate(vehicle) onUpdate?(vehicle)
} }
return return
} }
@ -90,7 +90,7 @@ class NotesViewModel: ACHudContainer {
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
onUpdate(vehicle) onUpdate?(vehicle)
} }
} }

View File

@ -21,7 +21,7 @@ class ReportViewModel: ACHudContainer {
var vehicle: VehicleDto var vehicle: VehicleDto
var hud: ACHud? var hud: ACHud?
let onUpdate: (VehicleDto) -> Void let onUpdate: ((VehicleDto) -> Void)?
var plateNumber: String { var plateNumber: String {
if vehicle.outdated, let current = vehicle.currentNumber { if vehicle.outdated, let current = vehicle.currentNumber {
@ -55,12 +55,14 @@ class ReportViewModel: ACHudContainer {
return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt) return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt)
} }
init(apiService: ApiServiceProtocol, init(
storageService: StorageServiceProtocol, apiService: ApiServiceProtocol,
settingsService: SettingsServiceProtocol, storageService: StorageServiceProtocol,
vehicle: VehicleDto, settingsService: SettingsServiceProtocol,
isPersistent: Bool, vehicle: VehicleDto,
onUpdate: @escaping (VehicleDto) -> Void) { isPersistent: Bool,
onUpdate: ((VehicleDto) -> Void)? = nil
) {
self.apiService = apiService self.apiService = apiService
self.storageService = storageService self.storageService = storageService
@ -97,12 +99,12 @@ class ReportViewModel: ACHudContainer {
guard let self else { throw GenericError.somethingWentWrong } guard let self else { throw GenericError.somethingWentWrong }
vehicle = try await apiService.checkVehicleGb(by: vehicle.getNumber()) vehicle = try await apiService.checkVehicleGb(by: vehicle.getNumber())
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists) try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
onUpdate(vehicle) onUpdate?(vehicle)
} }
} }
func onVehicleChanged(_ vehicle: VehicleDto) { func onVehicleChanged(_ vehicle: VehicleDto) {
self.vehicle = vehicle self.vehicle = vehicle
onUpdate(vehicle) onUpdate?(vehicle)
} }
} }

View File

@ -140,8 +140,8 @@ struct HistoryTests {
let updatedVehicle: VehicleDto = .normal.addNote(text: "123") let updatedVehicle: VehicleDto = .normal.addNote(text: "123")
given(storageServiceMock) given(storageServiceMock)
.loadVehicles() .loadVehicles().willReturn([.normal])
.willReturn([.normal]) .loadVehicles().willReturn([updatedVehicle])
given(storageServiceMock) given(storageServiceMock)
.dbFileURL .dbFileURL
@ -151,14 +151,6 @@ struct HistoryTests {
.updateHistory(number: .any) .updateHistory(number: .any)
.willReturn((vehicle: updatedVehicle, errors: [])) .willReturn((vehicle: updatedVehicle, errors: []))
when(storageServiceMock)
.loadVehicles()
.perform {
given(storageServiceMock)
.loadVehicles()
.willReturn([updatedVehicle])
}
await viewModel.onAppear() await viewModel.onAppear()
await viewModel.updateVehicle(.normal) await viewModel.updateVehicle(.normal)
@ -168,7 +160,7 @@ struct HistoryTests {
verify(storageServiceMock) verify(storageServiceMock)
.loadVehicles() .loadVehicles()
.called(.exactly(3)) .called(.exactly(2))
#expect(viewModel.vehicles.count == 1) #expect(viewModel.vehicles.count == 1)
#expect(viewModel.vehiclesFiltered.count == 1) #expect(viewModel.vehiclesFiltered.count == 1)

View File

@ -22,32 +22,30 @@ struct LocationPickerTests {
let longitude: CLLocationDegrees = 10 let longitude: CLLocationDegrees = 10
let address = "Test Address" let address = "Test Address"
let geocoderMock = MockGeocoderProtocol() let locationServiceMock = MockLocationServiceProtocol()
func makeViewModel(event: VehicleEventDto) -> LocationPickerViewModel { func makeViewModel(event: VehicleEventDto) -> LocationPickerViewModel {
let locationService = LocationService( return LocationPickerViewModel(
geocoder: geocoderMock, locationService: locationServiceMock,
locationManager: MockSwiftLocationProtocol(), event: event
settingsService: MockSettingsServiceProtocol()
) )
return LocationPickerViewModel(locationService: locationService,
event: event)
} }
@Test("Set initial location (user)") @Test("Set initial location (user)")
func setInitialLocationUser() async throws { func setInitialLocationUser() async throws {
let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil))
viewModel.onAppear()
#expect(viewModel.position == .userLocation(fallback: .automatic)) #expect(viewModel.position == .userLocation(fallback: .automatic))
} }
@Test("Set initial location (custom)") @Test("Set initial location (custom)")
func setInitialLocationCustom() async throws { func setInitialLocationCustom() async throws {
let viewModel = makeViewModel(event: .init(lat: latitude, lon: longitude, addedBy: nil)) 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.latitude == latitude)
#expect(viewModel.position.region?.center.longitude == longitude) #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 viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil))
let location = CLLocation(latitude: latitude, longitude: longitude) given(locationServiceMock)
let placemark = CLPlacemark(location: location, name: address, postalAddress: nil) .getAddressForLocation(latitude: .value(latitude), longitude: .value(longitude))
.willReturn(address)
given(geocoderMock)
.reverseGeocodeLocation(.any)
.willReturn([placemark])
await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude)) await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude))
@ -71,4 +66,32 @@ struct LocationPickerTests {
#expect(viewModel.event.longitude == longitude) #expect(viewModel.event.longitude == longitude)
#expect(viewModel.event.address == address) #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)
}
} }