Replacing UINavigationController with SwiftUI's NavigationStack

This commit is contained in:
Selim Mustafaev 2025-04-19 10:59:27 +03:00
parent 0ca4c232db
commit 180866c31b
56 changed files with 586 additions and 1114 deletions

View File

@ -14,11 +14,9 @@
7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A06E0B42C707E2B005731AC /* SettingsService.swift */; };
7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226B2C551EC500B84627 /* LocationEditScreen.swift */; };
7A10226E2C551EE000B84627 /* LocationEditViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */; };
7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */; };
7A1022722C554A1300B84627 /* CustomHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1022712C554A1300B84627 /* CustomHostingController.swift */; };
7A1022772C557EC400B84627 /* LocationPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1022762C557EC400B84627 /* LocationPickerScreen.swift */; };
7A1022792C557ED600B84627 /* LocationPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */; };
7A10227B2C557EE900B84627 /* LocationPickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */; };
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */; };
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470023FDE7E500B424AF /* AppDelegate.swift */; };
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470223FDE7E500B424AF /* SceneDelegate.swift */; };
@ -32,7 +30,6 @@
7A131FD72D37B77E00DC7755 /* HistoryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A131FD62D37B77E00DC7755 /* HistoryCoordinator.swift */; };
7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441652C297EDE00E79018 /* NotesScreen.swift */; };
7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441672C297EFD00E79018 /* NotesViewModel.swift */; };
7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416B2C297F2100E79018 /* NotesCoordinator.swift */; };
7A14416E2C297F7C00E79018 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416D2C297F7C00E79018 /* Coordinator.swift */; };
7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17CE492A2E820300626A6E /* UIStackView.swift */; };
7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17CE4B2A2E850200626A6E /* UISegmentedControl.swift */; };
@ -40,7 +37,6 @@
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */; };
7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F52CE900330004B740 /* ReportScreen.swift */; };
7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F72CE900440004B740 /* ReportViewModel.swift */; };
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */; };
7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78FE2CE91A740004B740 /* Vehicle.swift */; };
7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */; };
7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */; };
@ -48,14 +44,12 @@
7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3399AA299063370087DF98 /* SearchControllerExt.swift */; };
7A386A402DABDC190051676A /* MapScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A3F2DABDC190051676A /* MapScreen.swift */; };
7A386A442DABDC360051676A /* MapViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A432DABDC360051676A /* MapViewModel.swift */; };
7A386A462DABDC660051676A /* MapCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A452DABDC660051676A /* MapCoordinator.swift */; };
7A386A482DABE0D00051676A /* MapMarkerModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A386A472DABE0D00051676A /* MapMarkerModel.swift */; };
7A386A4B2DAC35F10051676A /* ClusterMap in Frameworks */ = {isa = PBXBuildFile; productRef = 7A386A4A2DAC35F10051676A /* ClusterMap */; };
7A386A4D2DAC35F10051676A /* ClusterMapSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 7A386A4C2DAC35F10051676A /* ClusterMapSwiftUI */; };
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; };
7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */; };
7A4322932CB2CCAA00085CF6 /* FiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */; };
7A4322952CB2CD0F00085CF6 /* FiltersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322942CB2CD0F00085CF6 /* FiltersCoordinator.swift */; };
7A45FB382C27073700618694 /* StorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45FB372C27073700618694 /* StorageService.swift */; };
7A4927D52CCE438600851C01 /* OptionalBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4927D42CCE438600851C01 /* OptionalBinding.swift */; };
7A4955822D58CCF900912E66 /* HistoryFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4955812D58CCF900912E66 /* HistoryFilter.swift */; };
@ -105,11 +99,8 @@
7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F095F26DBF588003A965D /* VehicleNote.swift */; };
7A7097C22C9EC139007CFDCA /* ServiceContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */; };
7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */; };
7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */; };
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158062C44085600852088 /* OsagoScreen.swift */; };
7A7158092C44087E00852088 /* OsagoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158082C44087E00852088 /* OsagoCoordinator.swift */; };
7A71580C2C44453200852088 /* AdsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71580B2C44453200852088 /* AdsScreen.swift */; };
7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71580D2C4445A200852088 /* AdsCoordinator.swift */; };
7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158112C444A6400852088 /* AdsViewModel.swift */; };
7A71EF572D0A26B200943129 /* EventModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A71EF562D0A26B200943129 /* EventModel.swift */; };
7A761C042677F18E0005F28F /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474323FF06CA00B424AF /* ApiService.swift */; };
@ -149,6 +140,7 @@
7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */; };
7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABB1F1267E9CC800D7AB32 /* SwiftDate */; };
7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */; };
7AADD4452DB2D4D60027FD7B /* MapInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AADD4442DB2D4D60027FD7B /* MapInput.swift */; };
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */; };
7AB490292D6B1217002F39C6 /* ACKeyboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */; };
7AB4902B2D6B1446002F39C6 /* ACKeyboardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */; };
@ -165,12 +157,9 @@
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; };
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; };
7AB9FE222D08C2A5005DE374 /* EventsScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9FE212D08C2A5005DE374 /* EventsScreen.swift */; };
7AB9FE262D08C2D7005DE374 /* EventsCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9FE252D08C2D7005DE374 /* EventsCoordinator.swift */; };
7AB9FE282D08C2F4005DE374 /* EventsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9FE272D08C2F4005DE374 /* EventsViewModel.swift */; };
7AB9FE2A2D08CF35005DE374 /* EventsScreenMode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB9FE292D08CF35005DE374 /* EventsScreenMode.swift */; };
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */; };
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */; };
7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */; };
7ABDA8032D8704F70083C715 /* VehicleRecordService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */; };
7ABDA8052D8705210083C715 /* VehicleRecordServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */; };
7ABDA8092D8710F80083C715 /* AutoCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA8082D8710F80083C715 /* AutoCancellable.swift */; };
@ -290,11 +279,9 @@
7A06E0B42C707E2B005731AC /* SettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsService.swift; sourceTree = "<group>"; };
7A10226B2C551EC500B84627 /* LocationEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditScreen.swift; sourceTree = "<group>"; };
7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditViewModel.swift; sourceTree = "<group>"; };
7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditCoordinator.swift; sourceTree = "<group>"; };
7A1022712C554A1300B84627 /* CustomHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHostingController.swift; sourceTree = "<group>"; };
7A1022762C557EC400B84627 /* LocationPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerScreen.swift; sourceTree = "<group>"; };
7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerViewModel.swift; sourceTree = "<group>"; };
7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerCoordinator.swift; sourceTree = "<group>"; };
7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellProgressView.swift; sourceTree = "<group>"; };
7A1146FD23FDE7E500B424AF /* AutoCat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AutoCat.app; sourceTree = BUILT_PRODUCTS_DIR; };
7A11470023FDE7E500B424AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -314,7 +301,6 @@
7A131FD62D37B77E00DC7755 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = "<group>"; };
7A1441652C297EDE00E79018 /* NotesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesScreen.swift; sourceTree = "<group>"; };
7A1441672C297EFD00E79018 /* NotesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesViewModel.swift; sourceTree = "<group>"; };
7A14416B2C297F2100E79018 /* NotesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesCoordinator.swift; sourceTree = "<group>"; };
7A14416D2C297F7C00E79018 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = "<group>"; };
7A15051124DB3E3000F39631 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = "<group>"; };
7A17CE492A2E820300626A6E /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = "<group>"; };
@ -323,7 +309,6 @@
7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBarButtonItem.swift; sourceTree = "<group>"; };
7A1E78F52CE900330004B740 /* ReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportScreen.swift; sourceTree = "<group>"; };
7A1E78F72CE900440004B740 /* ReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = "<group>"; };
7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCoordinator.swift; sourceTree = "<group>"; };
7A1E78FE2CE91A740004B740 /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
7A27ADF824A09CAD0035F39E /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = "<group>"; };
7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteAlertModifier.swift; sourceTree = "<group>"; };
@ -334,12 +319,10 @@
7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = "<group>"; };
7A386A3F2DABDC190051676A /* MapScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScreen.swift; sourceTree = "<group>"; };
7A386A432DABDC360051676A /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = "<group>"; };
7A386A452DABDC660051676A /* MapCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCoordinator.swift; sourceTree = "<group>"; };
7A386A472DABE0D00051676A /* MapMarkerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMarkerModel.swift; sourceTree = "<group>"; };
7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = "<group>"; };
7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersScreen.swift; sourceTree = "<group>"; };
7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; };
7A4322942CB2CD0F00085CF6 /* FiltersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersCoordinator.swift; sourceTree = "<group>"; };
7A43F9F7246C8A6200BA5B49 /* JWT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = "<group>"; };
7A45FB372C27073700618694 /* StorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageService.swift; sourceTree = "<group>"; };
7A4927D42CCE438600851C01 /* OptionalBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalBinding.swift; sourceTree = "<group>"; };
@ -399,11 +382,8 @@
7A6F095F26DBF588003A965D /* VehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNote.swift; sourceTree = "<group>"; };
7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceContainer.swift; sourceTree = "<group>"; };
7A7157FF2C43EA6900852088 /* OwnersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersScreen.swift; sourceTree = "<group>"; };
7A7158032C43EAA200852088 /* OwnersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersCoordinator.swift; sourceTree = "<group>"; };
7A7158062C44085600852088 /* OsagoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoScreen.swift; sourceTree = "<group>"; };
7A7158082C44087E00852088 /* OsagoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoCoordinator.swift; sourceTree = "<group>"; };
7A71580B2C44453200852088 /* AdsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsScreen.swift; sourceTree = "<group>"; };
7A71580D2C4445A200852088 /* AdsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsCoordinator.swift; sourceTree = "<group>"; };
7A7158112C444A6400852088 /* AdsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsViewModel.swift; sourceTree = "<group>"; };
7A71EF562D0A26B200943129 /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = "<group>"; };
7A761C0A267E8FF90005F28F /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
@ -437,6 +417,7 @@
7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = "<group>"; };
7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderModel.swift; sourceTree = "<group>"; };
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = "<group>"; };
7AADD4442DB2D4D60027FD7B /* MapInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapInput.swift; sourceTree = "<group>"; };
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = "<group>"; };
7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACKeyboardView.swift; sourceTree = "<group>"; };
@ -455,12 +436,9 @@
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
7AB9FE212D08C2A5005DE374 /* EventsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsScreen.swift; sourceTree = "<group>"; };
7AB9FE252D08C2D7005DE374 /* EventsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsCoordinator.swift; sourceTree = "<group>"; };
7AB9FE272D08C2F4005DE374 /* EventsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsViewModel.swift; sourceTree = "<group>"; };
7AB9FE292D08CF35005DE374 /* EventsScreenMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsScreenMode.swift; sourceTree = "<group>"; };
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = "<group>"; };
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = "<group>"; };
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = "<group>"; };
7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordService.swift; sourceTree = "<group>"; };
7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordServiceProtocol.swift; sourceTree = "<group>"; };
7ABDA8082D8710F80083C715 /* AutoCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellable.swift; sourceTree = "<group>"; };
@ -593,7 +571,6 @@
children = (
7A10226B2C551EC500B84627 /* LocationEditScreen.swift */,
7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */,
7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */,
);
path = LocationEditScreen;
sourceTree = "<group>";
@ -603,7 +580,6 @@
children = (
7A1022762C557EC400B84627 /* LocationPickerScreen.swift */,
7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */,
7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */,
);
path = LocationPickerScreen;
sourceTree = "<group>";
@ -755,7 +731,6 @@
children = (
7A1441652C297EDE00E79018 /* NotesScreen.swift */,
7A1441672C297EFD00E79018 /* NotesViewModel.swift */,
7A14416B2C297F2100E79018 /* NotesCoordinator.swift */,
7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */,
);
path = NotesScreen;
@ -766,7 +741,6 @@
children = (
7A1E78F52CE900330004B740 /* ReportScreen.swift */,
7A1E78F72CE900440004B740 /* ReportViewModel.swift */,
7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */,
);
path = ReportScreen;
sourceTree = "<group>";
@ -784,8 +758,8 @@
children = (
7A386A3F2DABDC190051676A /* MapScreen.swift */,
7A386A432DABDC360051676A /* MapViewModel.swift */,
7A386A452DABDC660051676A /* MapCoordinator.swift */,
7A386A472DABE0D00051676A /* MapMarkerModel.swift */,
7AADD4442DB2D4D60027FD7B /* MapInput.swift */,
);
path = MapScreen;
sourceTree = "<group>";
@ -811,7 +785,6 @@
children = (
7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */,
7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */,
7A4322942CB2CD0F00085CF6 /* FiltersCoordinator.swift */,
);
path = FiltersScreen;
sourceTree = "<group>";
@ -954,7 +927,6 @@
isa = PBXGroup;
children = (
7A7157FF2C43EA6900852088 /* OwnersScreen.swift */,
7A7158032C43EAA200852088 /* OwnersCoordinator.swift */,
);
path = OwnersScreen;
sourceTree = "<group>";
@ -963,7 +935,6 @@
isa = PBXGroup;
children = (
7A7158062C44085600852088 /* OsagoScreen.swift */,
7A7158082C44087E00852088 /* OsagoCoordinator.swift */,
);
path = OsagoScreen;
sourceTree = "<group>";
@ -972,7 +943,6 @@
isa = PBXGroup;
children = (
7A71580B2C44453200852088 /* AdsScreen.swift */,
7A71580D2C4445A200852088 /* AdsCoordinator.swift */,
7A7158112C444A6400852088 /* AdsViewModel.swift */,
);
path = AdsScreen;
@ -1075,7 +1045,6 @@
isa = PBXGroup;
children = (
7AB9FE212D08C2A5005DE374 /* EventsScreen.swift */,
7AB9FE252D08C2D7005DE374 /* EventsCoordinator.swift */,
7AB9FE272D08C2F4005DE374 /* EventsViewModel.swift */,
7AB9FE292D08CF35005DE374 /* EventsScreenMode.swift */,
7A71EF562D0A26B200943129 /* EventModel.swift */,
@ -1087,8 +1056,6 @@
isa = PBXGroup;
children = (
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */,
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */,
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */,
);
path = GalleryScreen;
sourceTree = "<group>";
@ -1451,7 +1418,6 @@
7A1022772C557EC400B84627 /* LocationPickerScreen.swift in Sources */,
7A4322932CB2CCAA00085CF6 /* FiltersViewModel.swift in Sources */,
7A5D7E0C2C71EB25002C17E7 /* ToggleRowView.swift in Sources */,
7A7158092C44087E00852088 /* OsagoCoordinator.swift in Sources */,
7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */,
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */,
7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */,
@ -1472,11 +1438,9 @@
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */,
7AF860702CBAA24500954D2F /* NavigationLink.swift in Sources */,
7A386A482DABE0D00051676A /* MapMarkerModel.swift in Sources */,
7AB9FE262D08C2D7005DE374 /* EventsCoordinator.swift in Sources */,
7A386A402DABDC190051676A /* MapScreen.swift in Sources */,
7AB9FE282D08C2F4005DE374 /* EventsViewModel.swift in Sources */,
7A4927D52CCE438600851C01 /* OptionalBinding.swift in Sources */,
7A386A462DABDC660051676A /* MapCoordinator.swift in Sources */,
7A5911EE2D63226F00EC51BA /* SearchScreen.swift in Sources */,
7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */,
7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */,
@ -1489,8 +1453,6 @@
7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */,
7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */,
7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */,
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */,
7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */,
7AF231952DA1C29300AE5EB3 /* AuthViewModel.swift in Sources */,
7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */,
7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */,
@ -1499,7 +1461,6 @@
7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */,
7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */,
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */,
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */,
7A386A442DABDC360051676A /* MapViewModel.swift in Sources */,
7ADFC9592DAD1C3D001A43E3 /* GoogleAuthViewModel.swift in Sources */,
7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */,
@ -1513,7 +1474,6 @@
7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */,
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */,
7AB9FE2A2D08CF35005DE374 /* EventsScreenMode.swift in Sources */,
7A10227B2C557EE900B84627 /* LocationPickerCoordinator.swift in Sources */,
7AB490292D6B1217002F39C6 /* ACKeyboardView.swift in Sources */,
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
@ -1526,7 +1486,6 @@
7ADFC95B2DAD1F45001A43E3 /* WebView.swift in Sources */,
7AB4902B2D6B1446002F39C6 /* ACKeyboardButton.swift in Sources */,
7AFBE8CE2C308B53003C491D /* ACMessageView.swift in Sources */,
7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */,
7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */,
7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */,
7AC3555229696E3F00889457 /* UIView+layout.swift in Sources */,
@ -1538,7 +1497,6 @@
7AFBE8C42C302561003C491D /* ACHudContainer.swift in Sources */,
7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */,
7A06E0AC2C7065AC005731AC /* SettingsScreen.swift in Sources */,
7A4322952CB2CD0F00085CF6 /* FiltersCoordinator.swift in Sources */,
7AF231992DA27C1B00AE5EB3 /* ACButtonView.swift in Sources */,
7A131FD72D37B77E00DC7755 /* HistoryCoordinator.swift in Sources */,
7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */,
@ -1546,6 +1504,7 @@
7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */,
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */,
7A71580C2C44453200852088 /* AdsScreen.swift in Sources */,
7AADD4452DB2D4D60027FD7B /* MapInput.swift in Sources */,
7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */,
7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */,
7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */,
@ -1553,11 +1512,8 @@
7AB9FE222D08C2A5005DE374 /* EventsScreen.swift in Sources */,
7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */,
7A5911F02D63266B00EC51BA /* SearchViewModel.swift in Sources */,
7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */,
7A589E0F2D6B6E8E00EF3FBE /* NumberEditView.swift in Sources */,
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */,
7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */,
7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */,
7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */,
7A9519802D80B6C100E69883 /* RecordsScreen.swift in Sources */,
7A131FD52D37B76A00DC7755 /* HistoryViewModel.swift in Sources */,

View File

@ -5,6 +5,7 @@ import PKHUD
import AutoCatCore
import SwiftLocation
import CoreLocation
import SwiftUI
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@ -164,9 +165,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
HUD.show(.progress)
let vehicle = try await apiService.getReport(for: number)
Task {
let coordinator = ReportCoordinator(controller: rootController, vehicle: vehicle, isPersistent: false)
_ = try? await coordinator.start()
Task {
let screen = ReportScreen(vehicle: vehicle, isPersistent: false, onUpdate: { _ in })
let controller = UIHostingController(rootView: screen)
let navController = UINavigationController(rootViewController: controller)
rootController.present(navController, animated: true)
}
HUD.hide()

View File

@ -1,30 +0,0 @@
//
// AdsCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 14.07.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
class AdsCoordinator: Coordinator {
let viewController: UINavigationController?
let ads: [VehicleAdDto]
init(navController: UINavigationController, ads: [VehicleAdDto]) {
self.viewController = navController
self.ads = ads
}
func start() async throws {
let viewModel = await AdsViewModel(ads: ads)
let controller = await UIHostingController(rootView: AdsScreen(viewModel: viewModel))
await viewController?.pushViewController(controller, animated: true)
}
}

View File

@ -7,6 +7,7 @@
//
import SwiftUI
import AutoCatCore
struct AdsScreen: View {
@ -14,6 +15,10 @@ struct AdsScreen: View {
@State var galleryModel: ACImageSliderModel?
init(ads: [VehicleAdDto]) {
self.viewModel = AdsViewModel(ads: ads)
}
var body: some View {
List(viewModel.ads, id: \.self) { ad in
Section(viewModel.dateStringFrom(ad.date)) {
@ -68,7 +73,3 @@ struct AdsScreen: View {
}
}
}
#Preview {
AdsScreen(viewModel: AdsViewModel(ads: []))
}

View File

@ -1,43 +0,0 @@
//
// EventsCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 10.12.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import AutoCatCore
@MainActor
class EventsCoordinator {
let navController: UINavigationController
init(navController: UINavigationController) {
self.navController = navController
}
func start(vehicle: VehicleDto) async -> VehicleDto {
let resolver = ServiceContainer.shared
let viewModel = EventsViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self),
settingsService: resolver.resolve(SettingsServiceProtocol.self),
vehicle: vehicle
)
viewModel.coordinator = self
let controller = CustomHostingController(rootView: EventsScreen(viewModel: viewModel))
navController.pushViewController(controller, animated: true)
await controller.waitForDisappear()
return viewModel.vehicle
}
func editEvent(event: VehicleEventDto) async -> VehicleEventDto? {
let coordinator = LocationEditCoordinator(navController: navController)
return await coordinator.start(event: event)
}
}

View File

@ -12,13 +12,27 @@ import MapKit
struct EventsScreen: View {
enum Screen: Hashable {
case newEvent(VehicleEventDto)
case editEvent(VehicleEventDto)
}
@State var viewModel: EventsViewModel
@State var selectedEvent: EventModel?
@State var deleteConfirmationPresented: Bool = false
@State var eventToDelete: EventModel?
init(viewModel: EventsViewModel) {
self.viewModel = viewModel
init(vehicle: VehicleDto, onUpdate: @escaping (VehicleDto) -> Void) {
let resolver = ServiceContainer.shared
self.viewModel = EventsViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self),
settingsService: resolver.resolve(SettingsServiceProtocol.self),
vehicle: vehicle,
onUpdate: onUpdate
)
}
var body: some View {
@ -39,8 +53,14 @@ struct EventsScreen: View {
}
}
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "plus") {
Task { await viewModel.addNewEvent() }
NavigationLink(
value: Screen.newEvent(VehicleEventDto(
lat: 0,
lon: 0,
addedBy: viewModel.settingsService.user.email
))
) {
Image(systemName: "plus")
}
}
ToolbarItem(placement: .primaryAction) {
@ -72,7 +92,14 @@ struct EventsScreen: View {
} message: {
Text("Are you sure you want to delete this event?")
}
.navigationDestination(for: Screen.self) { screen in
switch screen {
case .newEvent(let event):
LocationEditScreen( event: event, onUpdate: viewModel.onNewEvent)
case .editEvent(let event):
LocationEditScreen(event: event, onUpdate: viewModel.onEventUpdated)
}
}
}
var map: some View {
@ -125,10 +152,10 @@ struct EventsScreen: View {
@ViewBuilder
func makeActions(for event: EventModel, useLabels: Bool = false) -> some View {
Button() {
Task { await viewModel.editEvent(event) }
} label: {
Label(useLabels ? "Edit" : "", systemImage: "pencil")
if let dto = viewModel.vehicle.events.first(where: { $0.id == event.id }) {
NavigationLink(value: Screen.editEvent(dto)) {
Label(useLabels ? "Edit" : "", systemImage: "pencil")
}
}
Button() {
@ -157,14 +184,3 @@ struct EventsScreen: View {
}
}
}
#if DEBUG
#Preview {
EventsScreen(viewModel: .init(apiService: MockApiServiceProtocol(),
storageService: MockStorageServiceProtocol(),
settingsService: MockSettingsServiceProtocol(),
vehicle: .preview))
}
#endif

View File

@ -30,8 +30,6 @@ class EventsViewModel: ACHudContainer {
let storageService: StorageServiceProtocol
let settingsService: SettingsServiceProtocol
weak var coordinator: EventsCoordinator?
var hud: ACHud?
var vehicle: VehicleDto
@ -41,6 +39,8 @@ class EventsViewModel: ACHudContainer {
var showPasteAlert: Bool = false
var pastedEvent: VehicleEventDto?
var onUpdate: (VehicleDto) -> Void
var isPasteAvailable: Bool {
UIPasteboard.general.data(forPasteboardType: UTType.vehicleEvent.identifier) != nil
}
@ -53,12 +53,14 @@ class EventsViewModel: ACHudContainer {
init(apiService: ApiServiceProtocol,
storageService: StorageServiceProtocol,
settingsService: SettingsServiceProtocol,
vehicle: VehicleDto) {
vehicle: VehicleDto,
onUpdate: @escaping (VehicleDto) -> Void) {
self.apiService = apiService
self.storageService = storageService
self.settingsService = settingsService
self.vehicle = vehicle
self.onUpdate = onUpdate
updateEvents()
}
@ -82,6 +84,28 @@ class EventsViewModel: ACHudContainer {
user: event.addedBy
)
}
onUpdate(vehicle)
}
func onEventUpdated(_ event: VehicleEventDto) {
Task {
await eventOperation {
try await self.storageService.edit(event: event, for: self.vehicle.getNumber())
} apiOperation: {
try await self.apiService.edit(event: event)
}
updateEvents()
}
}
func onNewEvent(_ event: VehicleEventDto) {
Task {
await addEvent(event)
}
}
func addEvent(_ event: VehicleEventDto) async {
@ -95,14 +119,6 @@ class EventsViewModel: ACHudContainer {
updateEvents()
}
func addNewEvent() async {
let emptyEvent = VehicleEventDto(lat: 0, lon: 0, addedBy: settingsService.user.email)
if let newEvent = await coordinator?.editEvent(event: emptyEvent) {
await addEvent(newEvent)
}
}
func deleteEvent(_ event: EventModel) async {
await eventOperation {
@ -114,22 +130,6 @@ class EventsViewModel: ACHudContainer {
updateEvents()
}
func editEvent(_ event: EventModel) async {
guard let eventDto = vehicle.events.first(where: { $0.id == event.id }) else {
return
}
if let updatedEvent = await coordinator?.editEvent(event: eventDto) {
await eventOperation {
try await self.storageService.edit(event: updatedEvent, for: self.vehicle.getNumber())
} apiOperation: {
try await self.apiService.edit(event: updatedEvent)
}
updateEvents()
}
}
func eventOperation(_ storageOperation: @escaping VehicleOperation, apiOperation: @escaping VehicleOperation) async {
if vehicle.unrecognized {

View File

@ -1,34 +0,0 @@
//
// FiltersCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 06.10.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import AutoCatCore
import UIKit
@MainActor
class FiltersCoordinator {
let viewController: UINavigationController
let filter: Filter
init(navController: UINavigationController, filter: Filter) {
self.viewController = navController
self.filter = filter
}
func start() async -> Filter? {
let viewModel = FiltersViewModel(
apiService: ServiceContainer.shared.resolve(ApiServiceProtocol.self),
filter: filter
)
let controller = CustomHostingController(rootView: FiltersScreen(viewModel: viewModel))
viewController.pushViewController(controller, animated: true)
await controller.waitForDisappear()
return viewModel.filterResult
}
}

View File

@ -23,6 +23,14 @@ struct FiltersScreen: View {
@State var viewModel: FiltersViewModel
init(filter: Filter, onUpdate: @escaping (Filter) -> Void) {
self.viewModel = FiltersViewModel(
apiService: ServiceContainer.shared.resolve(ApiServiceProtocol.self),
filter: filter,
onUpdate: onUpdate
)
}
var body: some View {
Form {
Section("Main filters") {
@ -118,14 +126,3 @@ struct FiltersScreen: View {
}
}
}
#if DEBUG
#Preview {
FiltersScreen(viewModel: .init(
apiService: MockApiServiceProtocol(),
filter: Filter()
))
}
#endif

View File

@ -26,8 +26,6 @@ class FiltersViewModel {
}
}
@ObservationIgnored var filterResult: Filter?
var models: [StringOption] = [.any]
var brands: [StringOption] = [.any]
var colors: [StringOption] = [.any]
@ -35,10 +33,16 @@ class FiltersViewModel {
@ObservationIgnored var currentBrand: StringOption = .any
init(apiService: ApiServiceProtocol, filter: Filter) {
let onUpdate: (Filter) -> Void
init(
apiService: ApiServiceProtocol,
filter: Filter,
onUpdate: @escaping (Filter) -> Void
) {
self.apiService = apiService
self.filter = filter
self.onUpdate = onUpdate
}
func loadData() async {
@ -61,7 +65,7 @@ class FiltersViewModel {
}
func applyFilters() {
filterResult = filter
onUpdate(filter)
}
func nullifyTime(of date: Date?) -> Date? {

View File

@ -1,31 +0,0 @@
//
// GalleryCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 07.12.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
@MainActor
class GalleryCoordinator: Coordinator {
let viewController: UINavigationController
let photos: [VehiclePhotoDto]
init(navController: UINavigationController, photos: [VehiclePhotoDto]) {
self.viewController = navController
self.photos = photos
}
func start() async throws {
let viewModel = GalleryViewModel(photos: photos)
let controller = UIHostingController(rootView: GalleryScreen(viewModel: viewModel))
viewController.pushViewController(controller, animated: true)
}
}

View File

@ -11,14 +11,14 @@ import AutoCatCore
struct GalleryScreen: View {
@State var viewModel: GalleryViewModel
let photos: [VehiclePhotoDto]
@State var galleryModel: ACImageSliderModel?
var body: some View {
ScrollView {
LazyVGrid(columns: columns, spacing: 2) {
ForEach(viewModel.photos) { photo in
ForEach(photos) { photo in
ZStack {
AsyncImage(url: URL(string: photo.url)) { phase in
switch phase {
@ -43,7 +43,7 @@ struct GalleryScreen: View {
}
galleryModel = ACImageSliderModel(
urls: viewModel.photos.compactMap { URL(string: $0.url) },
urls: photos.compactMap { URL(string: $0.url) },
selected: url
)
}
@ -66,7 +66,3 @@ struct GalleryScreen: View {
]
}
}
#Preview {
GalleryScreen(viewModel: .init(photos: []))
}

View File

@ -1,21 +0,0 @@
//
// GalleryViewModel.swift
// AutoCat
//
// Created by Selim Mustafaev on 07.12.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import SwiftUI
import AutoCatCore
@MainActor
@Observable
class GalleryViewModel {
let photos: [VehiclePhotoDto]
init(photos: [VehiclePhotoDto]) {
self.photos = photos
}
}

View File

@ -13,8 +13,6 @@ import AutoCatCore
@MainActor
final class HistoryCoordinator {
var navController: UINavigationController?
func start() -> (UIViewController, HistoryViewModel) {
let resolver = ServiceContainer.shared
@ -24,22 +22,9 @@ final class HistoryCoordinator {
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
)
viewModel.coordinator = self
let view = HistoryScreen(viewModel: viewModel)
let controller = UIHostingController(rootView: view)
let navController = UINavigationController(rootViewController: controller)
self.navController = navController
return (navController, viewModel)
}
func openReport(vehicle: VehicleDto) async {
let coordinator = ReportCoordinator(controller: navController,
vehicle: vehicle,
isPersistent: true)
_ = try? await coordinator.start()
return (controller, viewModel)
}
}

View File

@ -16,69 +16,89 @@ struct HistoryScreen: View {
@State var exportSheetPresented = false
var body: some View {
List {
ForEach(viewModel.vehicleSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { vehicle in
VehicleCellView(vehicle: vehicle)
.onTapGesture {
Task { await viewModel.openReport(vehicle: vehicle) }
}
.swipeActions(allowsFullSwipe: false) {
makeActions(for: vehicle)
}
.contextMenu {
makeActions(for: vehicle, useLabels: true)
NavigationStack {
List {
ForEach(viewModel.vehicleSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { vehicle in
NavigationLink(value: vehicle) {
vehicleCell(vehicle)
}
}
}
}
}
}
.onAppear {
Task { await viewModel.onAppear() }
}
.hud($viewModel.hud)
.listStyle(.plain)
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""),
viewModel.vehiclesCount))
.searchable(text: $viewModel.searchText, prompt: "Search plate numbers")
.searchPresentationToolbarBehavior(.avoidHidingContent)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "square.and.arrow.up") {
exportSheetPresented = true
.onAppear {
Task { await viewModel.onAppear() }
}
.hud($viewModel.hud)
.listStyle(.plain)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""),
viewModel.vehiclesCount))
.searchable(text: $viewModel.searchText, prompt: "Search plate numbers")
.searchPresentationToolbarBehavior(.avoidHidingContent)
.autocorrectionDisabled()
.textInputAutocapitalization(.never)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "square.and.arrow.up") {
exportSheetPresented = true
}
}
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "line.horizontal.3.decrease") {
filterSheetPresented = true
}
}
}
ToolbarItem(placement: .primaryAction) {
Button("", systemImage: "line.horizontal.3.decrease") {
filterSheetPresented = true
.confirmationDialog("Filter check history", isPresented: $filterSheetPresented, titleVisibility: .visible) {
ForEach(HistoryFilter.allCases) { filter in
Button(filter.title) {
viewModel.filter = filter
viewModel.applyFilters()
}
}
}
}
.confirmationDialog("Filter check history", isPresented: $filterSheetPresented, titleVisibility: .visible) {
ForEach(HistoryFilter.allCases) { filter in
Button(filter.title) {
viewModel.filter = filter
viewModel.applyFilters()
.confirmationDialog("Export history as", isPresented: $exportSheetPresented, titleVisibility: .visible) {
ShareLink(item: viewModel.vehiclesArchive, preview: SharePreview(VehiclesArchive.fileName)) {
Text("CSV table")
}
if let dbUrl = viewModel.dbFileURL {
ShareLink(item: dbUrl) {
Text("Database file")
}
}
}
}
.confirmationDialog("Export history as", isPresented: $exportSheetPresented, titleVisibility: .visible) {
ShareLink(item: viewModel.vehiclesArchive, preview: SharePreview(VehiclesArchive.fileName)) {
Text("CSV table")
.navigationDestination(for: VehicleDto.self) { vehicle in
ReportScreen(
vehicle: vehicle,
isPersistent: true,
onUpdate: viewModel.onVehicleChanged
)
}
if let dbUrl = viewModel.dbFileURL {
ShareLink(item: dbUrl) {
Text("Database file")
}
.navigationDestination(item: $viewModel.vehicleToOpen) { vehicle in
ReportScreen(
vehicle: vehicle,
isPersistent: true,
onUpdate: viewModel.onVehicleChanged
)
}
}
}
func vehicleCell(_ vehicle: VehicleDto) -> some View {
VehicleCellView(vehicle: vehicle)
.swipeActions(allowsFullSwipe: false) {
makeActions(for: vehicle)
}
.contextMenu {
makeActions(for: vehicle, useLabels: true)
}
}
@ViewBuilder
func makeActions(for vehicle: VehicleDto, useLabels: Bool = false) -> some View {
@ -95,7 +115,3 @@ struct HistoryScreen: View {
}
}
}
//#Preview {
// HistoryScreen(viewModel: .init())
//}

View File

@ -23,7 +23,6 @@ final class HistoryViewModel: ACHudContainer {
let apiService: ApiServiceProtocol
let storageService: StorageServiceProtocol
let vehicleService: VehicleServiceProtocol
var coordinator: HistoryCoordinator?
var hud: ACHud?
@ -31,6 +30,8 @@ final class HistoryViewModel: ACHudContainer {
var vehiclesFiltered: [VehicleDto] = []
var vehicleSections: [DateSection<VehicleDto>] = []
var vehicleToOpen: VehicleDto?
var searchText: String = "" {
didSet {
if searchText != oldValue {
@ -75,11 +76,6 @@ final class HistoryViewModel: ACHudContainer {
applyFilters()
}
func openReport(vehicle: VehicleDto) async {
await coordinator?.openReport(vehicle: vehicle)
await loadVehicles()
}
func applyFilters() {
vehiclesFiltered = filterType(vehicles)
vehiclesFiltered = filterSearch(vehiclesFiltered)
@ -133,7 +129,7 @@ final class HistoryViewModel: ACHudContainer {
if errors.isEmpty {
hud = nil
if !vehicle.unrecognized {
await openReport(vehicle: vehicle)
vehicleToOpen = vehicle
}
} else {
showErrors(errors)
@ -162,4 +158,11 @@ final class HistoryViewModel: ACHudContainer {
func checkRecord(number: String, event: VehicleEventDto?) async {
await checkVehicle(number: number, type: .record(event))
}
func onVehicleChanged(_ vehicle: VehicleDto) {
Task {
await loadVehicles()
}
}
}

View File

@ -1,40 +0,0 @@
//
// LocationEditCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 27.07.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
@MainActor
final class LocationEditCoordinator {
let viewController: UINavigationController
init(navController: UINavigationController) {
self.viewController = navController
}
func start(event: VehicleEventDto) async -> VehicleEventDto? {
let viewModel = LocationEditViewModel(event: event)
viewModel.coordinator = self
let screen = LocationEditScreen(viewModel: viewModel)
let controller = CustomHostingController(rootView: screen)
viewController.pushViewController(controller, animated: true)
await controller.waitForDisappear()
return viewModel.result
}
func openLocationPicker(event: VehicleEventDto) async -> VehicleEventDto? {
let coordinator = LocationPickerCoordinator(navController: viewController,
event: event)
return try? await coordinator.start()
}
}

View File

@ -11,18 +11,30 @@ import AutoCatCore
struct LocationEditScreen: View {
enum Screen: String {
case locationPicker
}
@Environment(\.dismiss) var dismiss
@State var viewModel: LocationEditViewModel
init(event: VehicleEventDto, onUpdate: @escaping (VehicleEventDto) -> Void) {
self.viewModel = LocationEditViewModel(
event: event,
onUpdate: onUpdate
)
}
var body: some View {
List {
DatePicker("Date", selection: $viewModel.date)
.datePickerStyle(.compact)
TextRowView(title: "Location", value: viewModel.event.location)
.onTapGesture {
Task { await viewModel.pickLocation() }
}
NavigationLink(value: Screen.locationPicker) {
TextRowView(title: "Location", value: viewModel.event.location)
}
}
.navigationTitle("Edit event")
.toolbar {
@ -33,13 +45,8 @@ struct LocationEditScreen: View {
}
}
}
.navigationDestination(for: Screen.self) { _ in
LocationPickerScreen(event: viewModel.event, onUpdate: viewModel.onEventUpdated)
}
}
}
#Preview {
var event = VehicleEventDto(lat: 25.54984, lon: 36.34857, addedBy: "")
event.address = "Ул. Ленина, 123"
return LocationEditScreen(viewModel: .init(event: event))
}

View File

@ -13,26 +13,23 @@ import SwiftUI
@Observable
final class LocationEditViewModel {
weak var coordinator: LocationEditCoordinator?
var event: VehicleEventDto
var date: Date
var result: VehicleEventDto?
var onUpdate: (VehicleEventDto) -> Void
init(event: VehicleEventDto) {
init(event: VehicleEventDto, onUpdate: @escaping (VehicleEventDto) -> Void) {
self.event = event
self.date = Date(timeIntervalSince1970: event.date)
self.onUpdate = onUpdate
}
func done() {
event.date = date.timeIntervalSince1970
result = event
onUpdate(event)
}
func pickLocation() async {
if let newEvent = await coordinator?.openLocationPicker(event: event) {
event = newEvent
}
func onEventUpdated(_ event: VehicleEventDto) {
self.event = event
}
}

View File

@ -1,36 +0,0 @@
//
// LocationPickerCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 27.07.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
@MainActor
final class LocationPickerCoordinator: Coordinator {
let viewController: UINavigationController?
let event: VehicleEventDto
init(navController: UINavigationController?, event: VehicleEventDto) {
self.viewController = navController
self.event = event
}
func start() async throws -> VehicleEventDto? {
let viewModel = LocationPickerViewModel(
locationService: ServiceContainer.shared.resolve(LocationServiceProtocol.self),
event: event
)
let screen = LocationPickerScreen(viewModel: viewModel)
let controller = CustomHostingController(rootView: screen)
viewController?.pushViewController(controller, animated: true)
await controller.waitForDisappear()
return viewModel.result
}
}

View File

@ -16,6 +16,15 @@ struct LocationPickerScreen: View {
@State var viewModel: LocationPickerViewModel
init(event: VehicleEventDto, onUpdate: @escaping (VehicleEventDto) -> Void) {
self.viewModel = LocationPickerViewModel(
locationService: ServiceContainer.shared.resolve(LocationServiceProtocol.self),
event: event,
onUpdate: onUpdate
)
}
var body: some View {
ZStack {
Map(initialPosition: viewModel.position)
@ -45,16 +54,3 @@ struct LocationPickerScreen: View {
}
}
}
#Preview {
var event = VehicleEventDto(lat: 47.250049, lon: 39.711821, addedBy: nil)
event.address = "Ул. Ленина, 123"
let viewModel = LocationPickerViewModel(
locationService: ServiceContainer.shared.resolve(LocationServiceProtocol.self),
event: event
)
return LocationPickerScreen(viewModel: viewModel)
}

View File

@ -20,12 +20,17 @@ final class LocationPickerViewModel {
var event: VehicleEventDto
var position: MapCameraPosition
var result: VehicleEventDto?
var onUpdate: (VehicleEventDto) -> Void
init(locationService: LocationServiceProtocol, event: VehicleEventDto) {
init(
locationService: LocationServiceProtocol,
event: VehicleEventDto,
onUpdate: @escaping (VehicleEventDto) -> Void
) {
self.locationService = locationService
self.event = event
self.onUpdate = onUpdate
if event.latitude == 0 && event.longitude == 0 {
self.position = .userLocation(fallback: .automatic)
@ -43,6 +48,6 @@ final class LocationPickerViewModel {
}
func done() {
result = event
onUpdate(event)
}
}

View File

@ -1,45 +0,0 @@
//
// MapCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 13.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
enum MapInput {
case event(VehicleEventDto)
case filter(Filter)
}
@MainActor
final class MapCoordinator {
let navController: UINavigationController
init(navController: UINavigationController) {
self.navController = navController
}
func start(mapInput: MapInput) {
let resolver = ServiceContainer.shared
let viewModel = MapViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
mapInput: mapInput
)
let controller = UIHostingController(
rootView: MapScreen(viewModel: viewModel)
)
//controller.modalPresentationStyle = .fullScreen
controller.hidesBottomBarWhenPushed = true
navController.pushViewController(controller, animated: true)
}
}

View File

@ -0,0 +1,15 @@
//
// MapInput.swift
// AutoCat
//
// Created by Selim Mustafaev on 18.04.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import AutoCatCore
enum MapInput {
case event(VehicleEventDto)
case filter(Filter)
}

View File

@ -9,11 +9,20 @@
import SwiftUI
import MapKit
import ClusterMapSwiftUI
import AutoCatCore
struct MapScreen: View {
@State var viewModel: MapViewModel
init(mapInput: MapInput) {
self.viewModel = MapViewModel(
apiService: ServiceContainer.shared.resolve(ApiServiceProtocol.self),
mapInput: mapInput
)
}
var body: some View {
Map {
ForEach(viewModel.markers) {

View File

@ -1,38 +0,0 @@
//
// NotesCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 24.06.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import Foundation
import SwiftUI
import AutoCatCore
@MainActor
class NotesCoordinator: Coordinator {
let viewController: UINavigationController?
let vehicle: VehicleDto
init(navController: UINavigationController, vehicle: VehicleDto) {
self.viewController = navController
self.vehicle = vehicle
}
func start() async throws -> VehicleDto {
let resolver = ServiceContainer.shared
let viewModel = NotesViewModel(
storageService: resolver.resolve(StorageServiceProtocol.self),
apiService: resolver.resolve(ApiServiceProtocol.self),
vehicle: vehicle
)
let controller = CustomHostingController(rootView: NotesScreen(viewModel: viewModel))
viewController?.pushViewController(controller, animated: true)
await controller.waitForDisappear()
return viewModel.vehicle
}
}

View File

@ -18,6 +18,17 @@ struct NotesScreen: View {
@State var selectedNoteId = ""
@State var noteText = ""
init(vehicle: VehicleDto, onUpdate: @escaping (VehicleDto) -> Void) {
let resolver = ServiceContainer.shared
self.viewModel = NotesViewModel(
storageService: resolver.resolve(StorageServiceProtocol.self),
apiService: resolver.resolve(ApiServiceProtocol.self),
vehicle: vehicle,
onUpdate: onUpdate
)
}
var body: some View {
List(viewModel.vehicle.notes) { note in
@ -85,26 +96,3 @@ struct NotesScreen: View {
}
}
}
#if DEBUG
#Preview {
var vehicle = VehicleDto()
vehicle.notes = [
.init(text: "qwe", user: ""),
.init(text: "asdf", user: ""),
.init(text: "zxcv", user: "")
]
let vm = NotesViewModel(
storageService: MockStorageServiceProtocol(),
apiService: MockApiServiceProtocol(),
vehicle: vehicle
)
return NotesScreen(viewModel: vm)
}
#endif

View File

@ -21,13 +21,19 @@ class NotesViewModel: ACHudContainer {
var vehicle: VehicleDto
var hud: ACHud?
init(storageService: StorageServiceProtocol,
apiService: ApiServiceProtocol,
vehicle: VehicleDto) {
var onUpdate: (VehicleDto) -> Void
init(
storageService: StorageServiceProtocol,
apiService: ApiServiceProtocol,
vehicle: VehicleDto,
onUpdate: @escaping (VehicleDto) -> Void
) {
self.storageService = storageService
self.apiService = apiService
self.vehicle = vehicle
self.onUpdate = onUpdate
}
func addNote(text: String) async {
@ -67,6 +73,7 @@ class NotesViewModel: ACHudContainer {
await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { throw GenericError.somethingWentWrong }
vehicle = try await storageOp()
onUpdate(vehicle)
}
return
}
@ -76,6 +83,7 @@ class NotesViewModel: ACHudContainer {
let vehicle = try await apiOp()
try await storageService.updateVehicle(dto: vehicle, policy: .ifExists)
self.vehicle = vehicle
onUpdate(vehicle)
}
}

View File

@ -1,29 +0,0 @@
//
// OsagoCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 14.07.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
class OsagoCoordinator: Coordinator {
let viewController: UINavigationController?
let contracts: [OsagoDto]
init(navController: UINavigationController, contracts: [OsagoDto]) {
self.viewController = navController
self.contracts = contracts
}
func start() async throws {
let controller = await UIHostingController(rootView: OsagoScreen(contracts: contracts))
await viewController?.pushViewController(controller, animated: true)
}
}

View File

@ -1,29 +0,0 @@
//
// OwnersCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 14.07.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
class OwnersCoordinator: Coordinator {
let viewController: UINavigationController?
let ownerships: [VehicleOwnershipPeriodDto]
init(navController: UINavigationController, ownerships: [VehicleOwnershipPeriodDto]) {
self.viewController = navController
self.ownerships = ownerships
}
func start() async throws {
let controller = await UIHostingController(rootView: OwnersScreen(ownerships: ownerships))
await viewController?.pushViewController(controller, animated: true)
}
}

View File

@ -13,8 +13,6 @@ import AutoCatCore
@MainActor
final class RecordsCoordinator {
var navController = UINavigationController()
func start(output: RecordScreenOutput?) -> UIViewController {
let resolver = ServiceContainer.shared
@ -24,20 +22,9 @@ final class RecordsCoordinator {
recordPlayer: resolver.resolve(RecordPlayerServiceProtocol.self)
)
viewModel.coordinator = self
viewModel.output = output
let view = RecordsScreen(viewModel: viewModel)
let controller = UIHostingController(rootView: view)
let navController = UINavigationController(rootViewController: controller)
self.navController = navController
return navController
}
func showOnMap(event: VehicleEventDto) {
let coordinator = MapCoordinator(navController: navController)
coordinator.start(mapInput: .event(event))
return UIHostingController(rootView: view)
}
}

View File

@ -18,59 +18,65 @@ struct RecordsScreen: View {
@State var selectedRecordId: String = ""
var body: some View {
List {
ForEach(viewModel.recordSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { model in
AudioRecordView(
record: .init(
dto: model,
isPlaying: model.id == viewModel.playingRecord?.id
),
progress: viewModel.progress
) {
viewModel.onPlayTapped(record: model)
}
.listRowInsets(EdgeInsets())
.swipeActions(allowsFullSwipe: false) {
makeActions(for: model)
}
.contextMenu {
makeActions(for: model, useLabels: true)
NavigationStack {
List {
ForEach(viewModel.recordSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { model in
AudioRecordView(
record: .init(
dto: model,
isPlaying: model.id == viewModel.playingRecord?.id
),
progress: viewModel.progress
) {
viewModel.onPlayTapped(record: model)
}
.listRowInsets(EdgeInsets())
.swipeActions(allowsFullSwipe: false) {
makeActions(for: model)
}
.contextMenu {
makeActions(for: model, useLabels: true)
}
}
}
}
}
}
.listStyle(.inset)
.hud($viewModel.hud)
.navigationTitle("Voice records")
.onAppear {
Task { await viewModel.onAppear() }
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
Task { await viewModel.startRecording() }
} label: {
Image(systemName: "plus")
}
.alert("Recording...", isPresented: $viewModel.showRecordingAlert) {
Button("Cancel", role: .cancel) {
Task { await viewModel.cancelRecording() }
.listStyle(.inset)
.hud($viewModel.hud)
.navigationTitle("Voice records")
.navigationBarTitleDisplayMode(.inline)
.onAppear {
Task { await viewModel.onAppear() }
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
Task { await viewModel.startRecording() }
} label: {
Image(systemName: "plus")
}
Button("Done") {
Task { await viewModel.stopRecording() }
.alert("Recording...", isPresented: $viewModel.showRecordingAlert) {
Button("Cancel", role: .cancel) {
Task { await viewModel.cancelRecording() }
}
Button("Done") {
Task { await viewModel.stopRecording() }
}
}
}
}
}
.noteAlert(
title: String(localized: "Edit plate number"),
body: $numberText,
isPresented: $showEditAlert
) { text in
Task { await viewModel.editRecord(id: selectedRecordId, number: numberText) }
.noteAlert(
title: String(localized: "Edit plate number"),
body: $numberText,
isPresented: $showEditAlert
) { text in
Task { await viewModel.editRecord(id: selectedRecordId, number: numberText) }
}
.navigationDestination(for: VehicleEventDto.self) { event in
MapScreen(mapInput: .event(event))
}
}
}
@ -109,10 +115,8 @@ struct RecordsScreen: View {
Label("Show recognized text", systemImage: "textformat")
}
if record.event != nil {
Button {
viewModel.showOnMap(record)
} label: {
if let event = record.event {
NavigationLink(value: event) {
Label("Show on map", systemImage: "map")
}
}

View File

@ -16,7 +16,6 @@ final class RecordsViewModel: ACHudContainer {
let recordService: VehicleRecordServiceProtocol
let storageService: StorageServiceProtocol
let recordPlayer: RecordPlayerServiceProtocol
var coordinator: RecordsCoordinator?
weak var output: RecordScreenOutput?
var hud: ACHud?
@ -112,14 +111,6 @@ final class RecordsViewModel: ACHudContainer {
hud = .message(record.rawText)
}
func showOnMap(_ record: AudioRecordDto) {
guard let event = record.event else {
return
}
coordinator?.showOnMap(event: event)
}
func check(_ record: AudioRecordDto) {
guard let number = record.number else {
return

View File

@ -1,107 +0,0 @@
//
// ReportCoordinator.swift
// AutoCat
//
// Created by Selim Mustafaev on 16.11.2024.
// Copyright © 2024 Selim Mustafaev. All rights reserved.
//
import UIKit
import SwiftUI
import AutoCatCore
@MainActor
class ReportCoordinator {
let viewController: UIViewController?
let vehicle: VehicleDto
let isPersistent: Bool
weak var navController: UINavigationController?
init(controller: UIViewController?, vehicle: VehicleDto, isPersistent: Bool) {
self.viewController = controller
self.vehicle = vehicle
self.isPersistent = isPersistent
}
func start() async -> VehicleDto {
let resolver = ServiceContainer.shared
let viewModel = ReportViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self),
settingsService: resolver.resolve(SettingsServiceProtocol.self),
vehicle: vehicle,
isPersistent: isPersistent
)
viewModel.coordinator = self
let controller = CustomHostingController(rootView: ReportScreen(viewModel: viewModel))
let newNavController = UINavigationController(rootViewController: controller)
viewController?.showDetailViewController(newNavController, sender: self)
navController = controller.navigationController
await controller.waitForDisappear()
return viewModel.vehicle
}
func openEvents(vehicle: VehicleDto) async -> VehicleDto? {
guard let navController else {
return nil
}
let coordinator = EventsCoordinator(navController: navController)
return await coordinator.start(vehicle: vehicle)
}
func openOsago(contracts: [OsagoDto]) {
guard let navController else { return }
Task {
let coordinator = OsagoCoordinator(navController: navController, contracts: contracts)
try? await coordinator.start()
}
}
func openOwners(ownerships: [VehicleOwnershipPeriodDto]) {
guard let navController else {
return
}
Task {
let coordiantor = OwnersCoordinator(navController: navController, ownerships: ownerships)
try? await coordiantor.start()
}
}
func openNotes(vehicle: VehicleDto) async -> VehicleDto? {
guard let navController else {
return nil
}
let coordinator = NotesCoordinator(navController: navController, vehicle: vehicle)
return try? await coordinator.start()
}
func openAds(_ ads: [VehicleAdDto]) {
guard let navController else {
return
}
Task {
let coordinator = AdsCoordinator(navController: navController, ads: ads)
try? await coordinator.start()
}
}
func openPhotos(_ photos: [VehiclePhotoDto]) {
guard let navController else {
return
}
Task {
let coordinator = GalleryCoordinator(navController: navController, photos: photos)
try? await coordinator.start()
}
}
}

View File

@ -11,8 +11,31 @@ import AutoCatCore
struct ReportScreen: View {
enum Screen: String {
case events
case osago
case owners
case notes
case ads
case photos
}
@State var viewModel: ReportViewModel
init(vehicle: VehicleDto, isPersistent: Bool, onUpdate: @escaping (VehicleDto) -> Void) {
let resolver = ServiceContainer.shared
self.viewModel = ReportViewModel(
apiService: resolver.resolve(ApiServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self),
settingsService: resolver.resolve(SettingsServiceProtocol.self),
vehicle: vehicle,
isPersistent: isPersistent,
onUpdate: onUpdate
)
}
var body: some View {
Form {
Section {
@ -52,22 +75,34 @@ struct ReportScreen: View {
}
Section("History") {
LabeledContent("Events", value: String(viewModel.vehicle.events.count))
.navigationLink(onTap: viewModel.openEvents)
LabeledContent("OSAGO", value: String(viewModel.vehicle.osagoContracts.count))
.navigationLink(isActive: !viewModel.vehicle.osagoContracts.isEmpty,
onTap: viewModel.openOsago)
LabeledContent("Owners", value: String(viewModel.vehicle.ownershipPeriods.count))
.navigationLink(isActive: !viewModel.vehicle.ownershipPeriods.isEmpty,
onTap: viewModel.openOwners)
LabeledContent("Photos", value: String(viewModel.vehicle.photos.count))
.navigationLink(isActive: !viewModel.vehicle.photos.isEmpty,
onTap: viewModel.openPhotoGallery)
LabeledContent("Ads", value: String(viewModel.vehicle.ads.count))
.navigationLink(isActive: !viewModel.vehicle.ads.isEmpty,
onTap: viewModel.openAds)
LabeledContent("Notes", value: String(viewModel.vehicle.notes.count))
.navigationLink(onTap: viewModel.openNotes)
NavigationLink(value: Screen.events) {
LabeledContent("Events", value: String(viewModel.vehicle.events.count))
}
NavigationLink(value: Screen.osago) {
LabeledContent("OSAGO", value: String(viewModel.vehicle.osagoContracts.count))
}
.disabled(viewModel.vehicle.osagoContracts.isEmpty)
NavigationLink(value: Screen.owners) {
LabeledContent("Owners", value: String(viewModel.vehicle.ownershipPeriods.count))
}
.disabled(viewModel.vehicle.ownershipPeriods.isEmpty)
NavigationLink(value: Screen.photos) {
LabeledContent("Photos", value: String(viewModel.vehicle.photos.count))
}
.disabled(viewModel.vehicle.photos.isEmpty)
NavigationLink(value: Screen.ads) {
LabeledContent("Ads", value: String(viewModel.vehicle.ads.count))
}
.disabled(viewModel.vehicle.ads.isEmpty)
NavigationLink(value: Screen.notes) {
LabeledContent("Notes", value: String(viewModel.vehicle.notes.count))
}
}
if viewModel.showDebugInfo {
@ -95,6 +130,22 @@ struct ReportScreen: View {
ShareLink(item: link)
}
}
.navigationDestination(for: Screen.self) { screen in
switch screen {
case .events:
EventsScreen(vehicle: viewModel.vehicle, onUpdate: viewModel.onVehicleChanged)
case .osago:
OsagoScreen(contracts: viewModel.vehicle.osagoContracts)
case .owners:
OwnersScreen(ownerships: viewModel.vehicle.ownershipPeriods)
case .notes:
NotesScreen(vehicle: viewModel.vehicle, onUpdate: viewModel.onVehicleChanged)
case .ads:
AdsScreen(ads: viewModel.vehicle.ads)
case .photos:
GalleryScreen(photos: viewModel.vehicle.photos)
}
}
}
@ViewBuilder
@ -122,17 +173,3 @@ struct ReportScreen: View {
}
}
}
#if DEBUG
#Preview {
ReportScreen(viewModel: .init(
apiService: MockApiServiceProtocol(),
storageService: MockStorageServiceProtocol(),
settingsService: MockSettingsServiceProtocol(),
vehicle: .preview,
isPersistent: false
))
}
#endif

View File

@ -17,12 +17,12 @@ class ReportViewModel: ACHudContainer {
let storageService: StorageServiceProtocol
let settingsService: SettingsServiceProtocol
var coordinator: ReportCoordinator?
let isPersistent: Bool
var vehicle: VehicleDto
var hud: ACHud?
let onUpdate: (VehicleDto) -> Void
var plateNumber: String {
if vehicle.outdated, let current = vehicle.currentNumber {
"\(vehicle.number) (\(current))"
@ -59,13 +59,15 @@ class ReportViewModel: ACHudContainer {
storageService: StorageServiceProtocol,
settingsService: SettingsServiceProtocol,
vehicle: VehicleDto,
isPersistent: Bool) {
isPersistent: Bool,
onUpdate: @escaping (VehicleDto) -> Void) {
self.apiService = apiService
self.storageService = storageService
self.settingsService = settingsService
self.vehicle = vehicle
self.isPersistent = isPersistent
self.onUpdate = onUpdate
}
func onAppear() async {
@ -95,40 +97,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)
}
}
// MARK: Open detail screens
func openEvents() {
Task {
if let vehicle = await coordinator?.openEvents(vehicle: vehicle) {
self.vehicle = vehicle
}
}
}
func openOsago() {
coordinator?.openOsago(contracts: vehicle.osagoContracts)
}
func openOwners() {
coordinator?.openOwners(ownerships: vehicle.ownershipPeriods)
}
func openPhotoGallery() {
coordinator?.openPhotos(vehicle.photos)
}
func openNotes() {
Task {
if let vehicle = await coordinator?.openNotes(vehicle: vehicle) {
self.vehicle = vehicle
}
}
}
func openAds() {
coordinator?.openAds(vehicle.ads)
func onVehicleChanged(_ vehicle: VehicleDto) {
self.vehicle = vehicle
onUpdate(vehicle)
}
}

View File

@ -13,8 +13,6 @@ import AutoCatCore
@MainActor
final class SearchCoordinator {
var navController = UINavigationController()
func start() -> UIViewController {
let resolver = ServiceContainer.shared
@ -22,47 +20,8 @@ final class SearchCoordinator {
apiService: resolver.resolve(ApiServiceProtocol.self),
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
)
viewModel.coordinator = self
let view = SearchScreen(viewModel: viewModel)
let controller = UIHostingController(rootView: view)
let navController = UINavigationController(rootViewController: controller)
self.navController = navController
return navController
}
func openReport(vehicle: VehicleDto) async -> VehicleDto? {
let coordinator = ReportCoordinator(controller: navController,
vehicle: vehicle,
isPersistent: false)
return await coordinator.start()
}
func openFilterDetail(filter: Filter) async -> Filter? {
let coordinator = FiltersCoordinator(navController: navController, filter: filter)
return await coordinator.start()
}
func showOnMap(filter: Filter) {
let coordinator = MapCoordinator(navController: navController)
coordinator.start(mapInput: .filter(filter))
}
func export(url: URL) {
guard let currentController = navController.visibleViewController else {
return
}
#if targetEnvironment(macCatalyst)
let controller = UIDocumentPickerViewController(forExporting: [url])
currentController.present(controller, animated: true)
#else
let activityController = UIActivityViewController(activityItems: [url], applicationActivities: nil)
currentController.present(activityController, animated: true)
#endif
return UIHostingController(rootView: view)
}
}

View File

@ -11,32 +11,56 @@ import AutoCatCore
struct SearchScreen: View {
enum Screen: Hashable {
case report(VehicleDto)
case filter(Filter)
case map(Filter)
}
@State var viewModel: SearchViewModel
var body: some View {
List {
vehicles
if viewModel.hasMoreData && !viewModel.vehicleSections.isEmpty {
progressCell
NavigationStack {
List {
vehicles
if viewModel.hasMoreData && !viewModel.vehicleSections.isEmpty {
progressCell
}
}
}
.listStyle(.plain)
.hud($viewModel.hud)
.searchable(text: $viewModel.searchText, prompt: "Search plate numbers")
.searchPresentationToolbarBehavior(.avoidHidingContent)
.disableAutocorrection(true)
.autocapitalization(.none)
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""),
viewModel.vehiclesCount))
.onAppear {
Task { await viewModel.onAppear() }
}
.refreshable {
Task { await viewModel.reloadData() }
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
toolbarMenu
.listStyle(.plain)
.hud($viewModel.hud)
.searchable(text: $viewModel.searchText, prompt: "Search plate numbers")
.searchPresentationToolbarBehavior(.avoidHidingContent)
.disableAutocorrection(true)
.autocapitalization(.none)
.navigationBarTitleDisplayMode(.inline)
.navigationTitle(String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""),
viewModel.vehiclesCount))
.onAppear {
Task { await viewModel.onAppear() }
}
.refreshable {
Task { await viewModel.reloadData() }
}
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
toolbarMenu
}
}
.navigationDestination(for: Screen.self) { screen in
switch screen {
case .report(let vehicle):
ReportScreen(
vehicle: vehicle,
isPersistent: false,
onUpdate: viewModel.onVehicleChanged
)
case .filter(let filter):
FiltersScreen(filter: filter, onUpdate: viewModel.onFilterChanged)
case .map(let filter):
MapScreen(mapInput: .filter(filter))
}
}
}
}
@ -45,21 +69,24 @@ struct SearchScreen: View {
ForEach(viewModel.vehicleSections) { section in
Section(header: Text(section.header)) {
ForEach(section.elements) { vehicle in
VehicleCellView(vehicle: vehicle)
.onTapGesture {
Task { await viewModel.openReport(vehicle: vehicle) }
}
.swipeActions(allowsFullSwipe: false) {
makeActions(for: vehicle)
}
.contextMenu {
makeActions(for: vehicle, useLabels: true)
}
NavigationLink(value: Screen.report(vehicle)) {
vehicleCell(vehicle)
}
}
}
}
}
func vehicleCell(_ vehicle: VehicleDto) -> some View {
VehicleCellView(vehicle: vehicle)
.swipeActions(allowsFullSwipe: false) {
makeActions(for: vehicle)
}
.contextMenu {
makeActions(for: vehicle, useLabels: true)
}
}
var progressCell: some View {
HStack {
Spacer()
@ -74,11 +101,11 @@ struct SearchScreen: View {
var toolbarMenu: some View {
Menu("", systemImage: "ellipsis") {
Button("Filter results", systemImage: "line.horizontal.3.decrease") {
Task { await viewModel.openFilterDetail() }
NavigationLink(value: Screen.filter(viewModel.filter)) {
Label("Filter results", systemImage: "line.horizontal.3.decrease")
}
Button("Show on map", systemImage: "map") {
viewModel.showOnMap()
NavigationLink(value: Screen.map(viewModel.filter)) {
Label("Show on map", systemImage: "map")
}
ShareLink(item: viewModel.vehiclesArchive, preview: SharePreview(VehiclesArchive.fileName))
//ShareLink(items: [viewModel.vehiclesArchive])
@ -95,7 +122,3 @@ struct SearchScreen: View {
}
}
}
//#Preview {
// SearchScreen(viewModel: .init())
//}

View File

@ -16,7 +16,6 @@ final class SearchViewModel: ACHudContainer {
let apiService: ApiServiceProtocol
let vehicleService: VehicleServiceProtocol
var coordinator: SearchCoordinator?
var hud: ACHud?
@ -119,57 +118,6 @@ final class SearchViewModel: ACHudContainer {
}
}
func openReport(vehicle: VehicleDto) async {
guard let updatedVehicle = await coordinator?.openReport(vehicle: vehicle) else {
return
}
if let index = vehicles.firstIndex(where: { $0.number == updatedVehicle.number }) {
vehicles[index] = updatedVehicle
vehicleSections = vehicles.groupedByDate(type: .updatedDate)
}
}
func openFilterDetail() async {
guard let updatedFilter = await coordinator?.openFilterDetail(filter: filter) else {
return
}
filter = updatedFilter
resetData()
await wrapWithToast { [weak self] in
guard let self else { return }
try await loadSearchResults(filter: filter)
}
}
func showOnMap() {
coordinator?.showOnMap(filter: filter)
}
func exportSearchResults() async {
await wrapWithToast { [weak self] in
guard let self else {
return
}
let resp = try await apiService.getVehicles(with: filter, pageToken: nil, pageSize: 0)
let newLine = "\r\n"
var csvString = VehicleDto.csvHeader + newLine
for vehicle in resp.items {
csvString.append(vehicle.csvLine)
csvString.append(newLine)
}
let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv")
try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8)
coordinator?.export(url: tmpUrl)
}
}
func updateVehicle(_ vehicle: VehicleDto) async {
do {
hud = .progress
@ -189,4 +137,28 @@ final class SearchViewModel: ACHudContainer {
hud = .error(error)
}
}
func onVehicleChanged(_ vehicle: VehicleDto) {
if let index = vehicles.firstIndex(where: { $0.number == vehicle.number }) {
vehicles[index] = vehicle
vehicleSections = vehicles.groupedByDate(type: .updatedDate)
}
}
func onFilterChanged(_ filter: Filter) {
Task {
await updateWithFilter(filter)
}
}
func updateWithFilter(_ filter: Filter) async {
self.filter = filter
resetData()
await wrapWithToast { [weak self] in
guard let self else { return }
try await loadSearchResults(filter: filter)
}
}
}

View File

@ -24,8 +24,7 @@ class SettingsCoordinator {
let viewModel = SettingsViewModel(settingsService: ServiceContainer.shared.resolve(SettingsServiceProtocol.self))
viewModel.coordinator = self
let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel))
return UINavigationController(rootViewController: controller)
return UIHostingController(rootView: SettingsScreen(viewModel: viewModel))
}
func openAuthScreen() {

View File

@ -16,85 +16,79 @@ struct SettingsScreen: View {
@State var googleLoginSheetOpened = false
var body: some View {
Form {
Section {
TextRowView(title: "Version", value: Bundle.main.fullVersion)
}
Section("Profile") {
SimpleTextRowView(title: "AutoCat Account", value: viewModel.autocatEmail)
SimpleTextRowView(title: "Google",
value: viewModel.googleEmail ?? "Log In",
showArrow: true)
.onTapGesture {
if viewModel.googleAuthorized {
googleSheetOpened = true
} else {
googleLoginSheetOpened = true
NavigationStack {
Form {
Section {
TextRowView(title: "Version", value: Bundle.main.fullVersion)
}
Section("Profile") {
SimpleTextRowView(title: "AutoCat Account", value: viewModel.autocatEmail)
SimpleTextRowView(title: "Google",
value: viewModel.googleEmail ?? "Log In",
showArrow: true)
.onTapGesture {
if viewModel.googleAuthorized {
googleSheetOpened = true
} else {
googleLoginSheetOpened = true
}
}
if viewModel.canSignOut {
Button("Sign Out", action: viewModel.signOut)
}
}
if viewModel.canSignOut {
Button("Sign Out", action: viewModel.signOut)
}
}
Section {
Picker("Server", selection: $viewModel.settingService.backend) {
ForEach(Constants.Backend.allCases, id: \.self) { backend in
Text(backend.name)
Section {
Picker("Server", selection: $viewModel.settingService.backend) {
ForEach(Constants.Backend.allCases, id: \.self) { backend in
Text(backend.name)
}
}
}
}
Section("Plate number recognition") {
ToggleRowView(title: "Alternative order",
description: "Recognize plate numbers in alternative form. For example 'ЕВА 123 777' instead of 'Е123ВА 777'",
toggle: $viewModel.settingService.recognizeAlternativeOrder)
ToggleRowView(title: "Shortened numbers",
description: "If enabled, app will try to recognize shortened plate numbers (without region) and add default region",
toggle: $viewModel.settingService.recognizeShortenedNumbers)
if viewModel.settingService.recognizeShortenedNumbers {
LabeledContent("Default region") {
TextField("", text: $viewModel.settingService.defaultRegion)
.frame(width: 50)
.multilineTextAlignment(.trailing)
Section("Plate number recognition") {
ToggleRowView(title: "Alternative order",
description: "Recognize plate numbers in alternative form. For example 'ЕВА 123 777' instead of 'Е123ВА 777'",
toggle: $viewModel.settingService.recognizeAlternativeOrder)
ToggleRowView(title: "Shortened numbers",
description: "If enabled, app will try to recognize shortened plate numbers (without region) and add default region",
toggle: $viewModel.settingService.recognizeShortenedNumbers)
if viewModel.settingService.recognizeShortenedNumbers {
LabeledContent("Default region") {
TextField("", text: $viewModel.settingService.defaultRegion)
.frame(width: 50)
.multilineTextAlignment(.trailing)
}
}
ToggleRowView(title: "Beep before record",
description: "When enabled, you will hear short sound before starting audio recording. This will only work when audio record is started via Siri",
toggle: $viewModel.settingService.recordBeep)
}
Section("Debug") {
ToggleRowView(title: "Show debug info",
description: nil,
toggle: $viewModel.settingService.showDebugInfo)
}
ToggleRowView(title: "Beep before record",
description: "When enabled, you will hear short sound before starting audio recording. This will only work when audio record is started via Siri",
toggle: $viewModel.settingService.recordBeep)
}
Section("Debug") {
ToggleRowView(title: "Show debug info",
description: nil,
toggle: $viewModel.settingService.showDebugInfo)
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.inline)
.confirmationDialog(viewModel.googleUsername ?? "",
isPresented: $googleSheetOpened,
titleVisibility: .visible) {
Button("Sign Out", role: .destructive) {
viewModel.googleSignout()
}
} message: {
if let email = viewModel.googleEmail {
Text("You are currently signed in with email \(email). It will help to gather more data about vehicles.")
}
}
.sheet(isPresented: $googleLoginSheetOpened) {
GoogleAuthScreen()
}
}
.navigationTitle("Settings")
.confirmationDialog(viewModel.googleUsername ?? "",
isPresented: $googleSheetOpened,
titleVisibility: .visible) {
Button("Sign Out", role: .destructive) {
viewModel.googleSignout()
}
} message: {
if let email = viewModel.googleEmail {
Text("You are currently signed in with email \(email). It will help to gather more data about vehicles.")
}
}
.sheet(isPresented: $googleLoginSheetOpened) {
GoogleAuthScreen()
}
}
}
#if DEBUG
#Preview {
SettingsScreen(viewModel: .init(settingsService: MockSettingsServiceProtocol()))
}
#endif

View File

@ -37,7 +37,7 @@ struct NavigationLinkModifier: ViewModifier {
extension View {
func navigationLink(isActive: Bool = true, onTap: (() -> Void)?) -> some View {
func navigationLink(isActive: Bool = true, onTap: (() -> Void)? = nil) -> some View {
modifier(NavigationLinkModifier(onTap: onTap, isActive: isActive))
}
}

View File

@ -9,7 +9,7 @@
import Foundation
@propertyWrapper
public struct NullifyDate: Sendable {
public struct NullifyDate: Sendable, Equatable, Hashable {
public var wrappedValue: Date? {
didSet {

View File

@ -8,13 +8,13 @@
import Foundation
public enum DebugInfoStatus: Int, Sendable, Decodable, Equatable {
public enum DebugInfoStatus: Int, Sendable, Decodable, Equatable, Hashable {
case success = 0
case error = 1
case warning = 2
}
public struct DebugInfoDto: Decodable, Sendable, Equatable {
public struct DebugInfoDto: Decodable, Sendable, Equatable, Hashable {
public var autocod: DebugInfoEntryDto?
public var vin01vin: DebugInfoEntryDto?
@ -23,7 +23,7 @@ public struct DebugInfoDto: Decodable, Sendable, Equatable {
public var nomerogram: DebugInfoEntryDto?
}
public struct DebugInfoEntryDto: Decodable, Sendable, Equatable {
public struct DebugInfoEntryDto: Decodable, Sendable, Equatable, Hashable {
public var fields: Int64 = 0
public var error: String?

View File

@ -8,7 +8,7 @@
import Foundation
public struct OsagoDto: Decodable, Sendable, Equatable {
public struct OsagoDto: Decodable, Sendable, Equatable, Hashable {
public var date: TimeInterval = 0
public var number: String = ""

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleBrandDto: Decodable, Sendable, Equatable {
public struct VehicleBrandDto: Decodable, Sendable, Equatable, Hashable {
public var name: VehicleNameDto?
public var logo: String?

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleDto: Sendable, Equatable {
public struct VehicleDto: Sendable, Equatable, Hashable {
public var brand: VehicleBrandDto?
public var model: VehicleModelDto?

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleEngineDto: Decodable, Sendable, Equatable {
public struct VehicleEngineDto: Decodable, Sendable, Equatable, Hashable {
public var number: String?
public var volume: Int? = 0

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleEventDto: Codable, Sendable, Equatable {
public struct VehicleEventDto: Codable, Sendable, Equatable, Hashable {
public var id: String = UUID().uuidString
public var date: TimeInterval = Date().timeIntervalSince1970

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleModelDto: Decodable, Sendable, Equatable {
public struct VehicleModelDto: Decodable, Sendable, Equatable, Hashable {
public var name: VehicleNameDto?
}

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleNameDto: Decodable, Sendable, Equatable {
public struct VehicleNameDto: Decodable, Sendable, Equatable, Hashable {
public var original: String?
public var normalized: String?

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleNoteDto: Codable, Sendable, Identifiable, Equatable {
public struct VehicleNoteDto: Codable, Sendable, Identifiable, Equatable, Hashable {
public var id: String = UUID().uuidString
public var user: String = ""

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehicleOwnershipPeriodDto: Decodable, Sendable, Equatable {
public struct VehicleOwnershipPeriodDto: Decodable, Sendable, Equatable, Hashable {
public var lastOperation: String = ""
public var ownerType: String = ""

View File

@ -8,7 +8,7 @@
import Foundation
public struct VehiclePhotoDto: Decodable, Sendable, Equatable, Identifiable {
public struct VehiclePhotoDto: Decodable, Sendable, Equatable, Identifiable, Hashable {
public let id = UUID()
public var brand: String?

View File

@ -38,7 +38,7 @@ public enum SortOrder: String, CustomStringConvertible, CaseIterable, Sendable {
}
}
public enum SearchScope: Int, CaseIterable, Sendable {
public enum SearchScope: Int, CaseIterable, Sendable, Hashable {
case plateNumber = 0
case vin = 1
@ -88,7 +88,7 @@ public enum StringOption: Hashable, Sendable {
}
}
public struct Filter: Sendable {
public struct Filter: Sendable, Hashable {
public var searchString = ""
public var brand: StringOption = .any
public var model: StringOption = .any

View File

@ -60,6 +60,18 @@ public final class VehiclesArchive {
vehicles = result.items
}
}
func makeCsvData() async throws -> Data {
try await loadVehiclesIfNeeded()
let csvString = try makeCsvString()
if let data = csvString.data(using: .utf8){
return data
} else {
throw VehiclesArchiveError.filedCreateCsv
}
}
}
extension VehiclesArchive: Transferable {
@ -72,15 +84,16 @@ extension VehiclesArchive: Transferable {
DataRepresentation(exportedContentType: .commaSeparatedText) { archive in
try await archive.loadVehiclesIfNeeded()
let csvString = try archive.makeCsvString()
if let data = csvString.data(using: .utf8){
return data
} else {
throw VehiclesArchiveError.filedCreateCsv
}
try await archive.makeCsvData()
}
.suggestedFileName(fileName)
FileRepresentation(exportedContentType: .commaSeparatedText) { archive in
let data = try await archive.makeCsvData()
let url = FileManager.default.tmpUrl(name: "search", ext: "csv")
try data.write(to: url)
return SentTransferredFile(url, allowAccessingOriginalFile: false)
}
}
}