diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index c6f43de..2cb8ee6 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -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 = ""; }; 7A10226B2C551EC500B84627 /* LocationEditScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditScreen.swift; sourceTree = ""; }; 7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditViewModel.swift; sourceTree = ""; }; - 7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationEditCoordinator.swift; sourceTree = ""; }; 7A1022712C554A1300B84627 /* CustomHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomHostingController.swift; sourceTree = ""; }; 7A1022762C557EC400B84627 /* LocationPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerScreen.swift; sourceTree = ""; }; 7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerViewModel.swift; sourceTree = ""; }; - 7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerCoordinator.swift; sourceTree = ""; }; 7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellProgressView.swift; sourceTree = ""; }; 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 = ""; }; @@ -314,7 +301,6 @@ 7A131FD62D37B77E00DC7755 /* HistoryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryCoordinator.swift; sourceTree = ""; }; 7A1441652C297EDE00E79018 /* NotesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesScreen.swift; sourceTree = ""; }; 7A1441672C297EFD00E79018 /* NotesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesViewModel.swift; sourceTree = ""; }; - 7A14416B2C297F2100E79018 /* NotesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesCoordinator.swift; sourceTree = ""; }; 7A14416D2C297F7C00E79018 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; 7A15051124DB3E3000F39631 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; 7A17CE492A2E820300626A6E /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = ""; }; @@ -323,7 +309,6 @@ 7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBarButtonItem.swift; sourceTree = ""; }; 7A1E78F52CE900330004B740 /* ReportScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportScreen.swift; sourceTree = ""; }; 7A1E78F72CE900440004B740 /* ReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; - 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCoordinator.swift; sourceTree = ""; }; 7A1E78FE2CE91A740004B740 /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = ""; }; 7A27ADF824A09CAD0035F39E /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = ""; }; 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteAlertModifier.swift; sourceTree = ""; }; @@ -334,12 +319,10 @@ 7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = ""; }; 7A386A3F2DABDC190051676A /* MapScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapScreen.swift; sourceTree = ""; }; 7A386A432DABDC360051676A /* MapViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewModel.swift; sourceTree = ""; }; - 7A386A452DABDC660051676A /* MapCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapCoordinator.swift; sourceTree = ""; }; 7A386A472DABE0D00051676A /* MapMarkerModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapMarkerModel.swift; sourceTree = ""; }; 7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = ""; }; 7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersScreen.swift; sourceTree = ""; }; 7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = ""; }; - 7A4322942CB2CD0F00085CF6 /* FiltersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersCoordinator.swift; sourceTree = ""; }; 7A43F9F7246C8A6200BA5B49 /* JWT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = ""; }; 7A45FB372C27073700618694 /* StorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageService.swift; sourceTree = ""; }; 7A4927D42CCE438600851C01 /* OptionalBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalBinding.swift; sourceTree = ""; }; @@ -399,11 +382,8 @@ 7A6F095F26DBF588003A965D /* VehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNote.swift; sourceTree = ""; }; 7A7097C12C9EC139007CFDCA /* ServiceContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServiceContainer.swift; sourceTree = ""; }; 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersScreen.swift; sourceTree = ""; }; - 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersCoordinator.swift; sourceTree = ""; }; 7A7158062C44085600852088 /* OsagoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoScreen.swift; sourceTree = ""; }; - 7A7158082C44087E00852088 /* OsagoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoCoordinator.swift; sourceTree = ""; }; 7A71580B2C44453200852088 /* AdsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsScreen.swift; sourceTree = ""; }; - 7A71580D2C4445A200852088 /* AdsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsCoordinator.swift; sourceTree = ""; }; 7A7158112C444A6400852088 /* AdsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsViewModel.swift; sourceTree = ""; }; 7A71EF562D0A26B200943129 /* EventModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventModel.swift; sourceTree = ""; }; 7A761C0A267E8FF90005F28F /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; @@ -437,6 +417,7 @@ 7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = ""; }; 7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderModel.swift; sourceTree = ""; }; 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = ""; }; + 7AADD4442DB2D4D60027FD7B /* MapInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapInput.swift; sourceTree = ""; }; 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = ""; }; 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = ""; }; 7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACKeyboardView.swift; sourceTree = ""; }; @@ -455,12 +436,9 @@ 7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; 7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; 7AB9FE212D08C2A5005DE374 /* EventsScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsScreen.swift; sourceTree = ""; }; - 7AB9FE252D08C2D7005DE374 /* EventsCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsCoordinator.swift; sourceTree = ""; }; 7AB9FE272D08C2F4005DE374 /* EventsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsViewModel.swift; sourceTree = ""; }; 7AB9FE292D08CF35005DE374 /* EventsScreenMode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsScreenMode.swift; sourceTree = ""; }; 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = ""; }; - 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = ""; }; - 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = ""; }; 7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordService.swift; sourceTree = ""; }; 7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordServiceProtocol.swift; sourceTree = ""; }; 7ABDA8082D8710F80083C715 /* AutoCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellable.swift; sourceTree = ""; }; @@ -593,7 +571,6 @@ children = ( 7A10226B2C551EC500B84627 /* LocationEditScreen.swift */, 7A10226D2C551EE000B84627 /* LocationEditViewModel.swift */, - 7A10226F2C551EFD00B84627 /* LocationEditCoordinator.swift */, ); path = LocationEditScreen; sourceTree = ""; @@ -603,7 +580,6 @@ children = ( 7A1022762C557EC400B84627 /* LocationPickerScreen.swift */, 7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */, - 7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */, ); path = LocationPickerScreen; sourceTree = ""; @@ -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 = ""; @@ -784,8 +758,8 @@ children = ( 7A386A3F2DABDC190051676A /* MapScreen.swift */, 7A386A432DABDC360051676A /* MapViewModel.swift */, - 7A386A452DABDC660051676A /* MapCoordinator.swift */, 7A386A472DABE0D00051676A /* MapMarkerModel.swift */, + 7AADD4442DB2D4D60027FD7B /* MapInput.swift */, ); path = MapScreen; sourceTree = ""; @@ -811,7 +785,6 @@ children = ( 7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */, 7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */, - 7A4322942CB2CD0F00085CF6 /* FiltersCoordinator.swift */, ); path = FiltersScreen; sourceTree = ""; @@ -954,7 +927,6 @@ isa = PBXGroup; children = ( 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */, - 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */, ); path = OwnersScreen; sourceTree = ""; @@ -963,7 +935,6 @@ isa = PBXGroup; children = ( 7A7158062C44085600852088 /* OsagoScreen.swift */, - 7A7158082C44087E00852088 /* OsagoCoordinator.swift */, ); path = OsagoScreen; sourceTree = ""; @@ -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 = ""; @@ -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 */, diff --git a/AutoCat/SceneDelegate.swift b/AutoCat/SceneDelegate.swift index bf62734..b70a9db 100644 --- a/AutoCat/SceneDelegate.swift +++ b/AutoCat/SceneDelegate.swift @@ -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() diff --git a/AutoCat/Screens/AdsScreen/AdsCoordinator.swift b/AutoCat/Screens/AdsScreen/AdsCoordinator.swift deleted file mode 100644 index f7edf5e..0000000 --- a/AutoCat/Screens/AdsScreen/AdsCoordinator.swift +++ /dev/null @@ -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) - } -} diff --git a/AutoCat/Screens/AdsScreen/AdsScreen.swift b/AutoCat/Screens/AdsScreen/AdsScreen.swift index 2e7f347..6b782cb 100644 --- a/AutoCat/Screens/AdsScreen/AdsScreen.swift +++ b/AutoCat/Screens/AdsScreen/AdsScreen.swift @@ -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: [])) -} diff --git a/AutoCat/Screens/EventsScreen/EventsCoordinator.swift b/AutoCat/Screens/EventsScreen/EventsCoordinator.swift deleted file mode 100644 index 0c72d9b..0000000 --- a/AutoCat/Screens/EventsScreen/EventsCoordinator.swift +++ /dev/null @@ -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) - } -} diff --git a/AutoCat/Screens/EventsScreen/EventsScreen.swift b/AutoCat/Screens/EventsScreen/EventsScreen.swift index 7e24114..6859490 100644 --- a/AutoCat/Screens/EventsScreen/EventsScreen.swift +++ b/AutoCat/Screens/EventsScreen/EventsScreen.swift @@ -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 diff --git a/AutoCat/Screens/EventsScreen/EventsViewModel.swift b/AutoCat/Screens/EventsScreen/EventsViewModel.swift index 722b602..2a0018e 100644 --- a/AutoCat/Screens/EventsScreen/EventsViewModel.swift +++ b/AutoCat/Screens/EventsScreen/EventsViewModel.swift @@ -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 { diff --git a/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift b/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift deleted file mode 100644 index fc54eb0..0000000 --- a/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift +++ /dev/null @@ -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 - } -} diff --git a/AutoCat/Screens/FiltersScreen/FiltersScreen.swift b/AutoCat/Screens/FiltersScreen/FiltersScreen.swift index 337b00e..77848c4 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersScreen.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersScreen.swift @@ -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 diff --git a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift index 52a57a6..8e67412 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift @@ -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? { diff --git a/AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift b/AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift deleted file mode 100644 index fa82f53..0000000 --- a/AutoCat/Screens/GalleryScreen/GalleryCoordinator.swift +++ /dev/null @@ -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) - } -} diff --git a/AutoCat/Screens/GalleryScreen/GalleryScreen.swift b/AutoCat/Screens/GalleryScreen/GalleryScreen.swift index 7497d87..63fb451 100644 --- a/AutoCat/Screens/GalleryScreen/GalleryScreen.swift +++ b/AutoCat/Screens/GalleryScreen/GalleryScreen.swift @@ -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: [])) -} diff --git a/AutoCat/Screens/GalleryScreen/GalleryViewModel.swift b/AutoCat/Screens/GalleryScreen/GalleryViewModel.swift deleted file mode 100644 index 8616bac..0000000 --- a/AutoCat/Screens/GalleryScreen/GalleryViewModel.swift +++ /dev/null @@ -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 - } -} diff --git a/AutoCat/Screens/HistoryScreen/HistoryCoordinator.swift b/AutoCat/Screens/HistoryScreen/HistoryCoordinator.swift index 12db1dd..266969a 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryCoordinator.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryCoordinator.swift @@ -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) } } diff --git a/AutoCat/Screens/HistoryScreen/HistoryScreen.swift b/AutoCat/Screens/HistoryScreen/HistoryScreen.swift index 8ea1a78..828d632 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryScreen.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryScreen.swift @@ -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()) -//} diff --git a/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift b/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift index 04dc295..1d13120 100644 --- a/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift +++ b/AutoCat/Screens/HistoryScreen/HistoryViewModel.swift @@ -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] = [] + 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() + } + } } diff --git a/AutoCat/Screens/LocationEditScreen/LocationEditCoordinator.swift b/AutoCat/Screens/LocationEditScreen/LocationEditCoordinator.swift deleted file mode 100644 index ee122d4..0000000 --- a/AutoCat/Screens/LocationEditScreen/LocationEditCoordinator.swift +++ /dev/null @@ -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() - } -} diff --git a/AutoCat/Screens/LocationEditScreen/LocationEditScreen.swift b/AutoCat/Screens/LocationEditScreen/LocationEditScreen.swift index e30d0e2..9e524ca 100644 --- a/AutoCat/Screens/LocationEditScreen/LocationEditScreen.swift +++ b/AutoCat/Screens/LocationEditScreen/LocationEditScreen.swift @@ -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)) -} diff --git a/AutoCat/Screens/LocationEditScreen/LocationEditViewModel.swift b/AutoCat/Screens/LocationEditScreen/LocationEditViewModel.swift index 29fc8dd..b63afc8 100644 --- a/AutoCat/Screens/LocationEditScreen/LocationEditViewModel.swift +++ b/AutoCat/Screens/LocationEditScreen/LocationEditViewModel.swift @@ -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 } } diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift deleted file mode 100644 index 94ec031..0000000 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift +++ /dev/null @@ -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 - } -} diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift index 846f410..b3599c4 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift @@ -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) -} diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift index 2642e38..21794cc 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift @@ -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) } } diff --git a/AutoCat/Screens/MapScreen/MapCoordinator.swift b/AutoCat/Screens/MapScreen/MapCoordinator.swift deleted file mode 100644 index 27fe78d..0000000 --- a/AutoCat/Screens/MapScreen/MapCoordinator.swift +++ /dev/null @@ -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) - } -} diff --git a/AutoCat/Screens/MapScreen/MapInput.swift b/AutoCat/Screens/MapScreen/MapInput.swift new file mode 100644 index 0000000..879162c --- /dev/null +++ b/AutoCat/Screens/MapScreen/MapInput.swift @@ -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) +} diff --git a/AutoCat/Screens/MapScreen/MapScreen.swift b/AutoCat/Screens/MapScreen/MapScreen.swift index f8b74ba..a81003b 100644 --- a/AutoCat/Screens/MapScreen/MapScreen.swift +++ b/AutoCat/Screens/MapScreen/MapScreen.swift @@ -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) { diff --git a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift deleted file mode 100644 index 7512ad5..0000000 --- a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift +++ /dev/null @@ -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 - } -} diff --git a/AutoCat/Screens/NotesScreen/NotesScreen.swift b/AutoCat/Screens/NotesScreen/NotesScreen.swift index c73b652..a4c749d 100644 --- a/AutoCat/Screens/NotesScreen/NotesScreen.swift +++ b/AutoCat/Screens/NotesScreen/NotesScreen.swift @@ -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 diff --git a/AutoCat/Screens/NotesScreen/NotesViewModel.swift b/AutoCat/Screens/NotesScreen/NotesViewModel.swift index b206829..a4e1cf0 100644 --- a/AutoCat/Screens/NotesScreen/NotesViewModel.swift +++ b/AutoCat/Screens/NotesScreen/NotesViewModel.swift @@ -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) } } diff --git a/AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift b/AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift deleted file mode 100644 index 85c7459..0000000 --- a/AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift +++ /dev/null @@ -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) - } -} diff --git a/AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift b/AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift deleted file mode 100644 index 9a3b7d8..0000000 --- a/AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift +++ /dev/null @@ -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) - } -} diff --git a/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift b/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift index 23e0624..540b67a 100644 --- a/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift +++ b/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift @@ -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) } } diff --git a/AutoCat/Screens/RecordsScreen/RecordsScreen.swift b/AutoCat/Screens/RecordsScreen/RecordsScreen.swift index 479ef25..b2aa035 100644 --- a/AutoCat/Screens/RecordsScreen/RecordsScreen.swift +++ b/AutoCat/Screens/RecordsScreen/RecordsScreen.swift @@ -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") } } diff --git a/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift b/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift index d8a719d..e9fb9ca 100644 --- a/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift +++ b/AutoCat/Screens/RecordsScreen/RecordsViewModel.swift @@ -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 diff --git a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift deleted file mode 100644 index b5d7024..0000000 --- a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift +++ /dev/null @@ -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() - } - } -} diff --git a/AutoCat/Screens/ReportScreen/ReportScreen.swift b/AutoCat/Screens/ReportScreen/ReportScreen.swift index 90aa8b7..450b902 100644 --- a/AutoCat/Screens/ReportScreen/ReportScreen.swift +++ b/AutoCat/Screens/ReportScreen/ReportScreen.swift @@ -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 diff --git a/AutoCat/Screens/ReportScreen/ReportViewModel.swift b/AutoCat/Screens/ReportScreen/ReportViewModel.swift index dd00832..13d3a55 100644 --- a/AutoCat/Screens/ReportScreen/ReportViewModel.swift +++ b/AutoCat/Screens/ReportScreen/ReportViewModel.swift @@ -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) } } diff --git a/AutoCat/Screens/SearchScreen/SearchCoordinator.swift b/AutoCat/Screens/SearchScreen/SearchCoordinator.swift index 7b5ac0c..72f8684 100644 --- a/AutoCat/Screens/SearchScreen/SearchCoordinator.swift +++ b/AutoCat/Screens/SearchScreen/SearchCoordinator.swift @@ -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) } } diff --git a/AutoCat/Screens/SearchScreen/SearchScreen.swift b/AutoCat/Screens/SearchScreen/SearchScreen.swift index a10a329..3b40d6f 100644 --- a/AutoCat/Screens/SearchScreen/SearchScreen.swift +++ b/AutoCat/Screens/SearchScreen/SearchScreen.swift @@ -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()) -//} diff --git a/AutoCat/Screens/SearchScreen/SearchViewModel.swift b/AutoCat/Screens/SearchScreen/SearchViewModel.swift index 800fb6c..2b3a336 100644 --- a/AutoCat/Screens/SearchScreen/SearchViewModel.swift +++ b/AutoCat/Screens/SearchScreen/SearchViewModel.swift @@ -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) + } + } } diff --git a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift index f66b88d..afe1374 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift @@ -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() { diff --git a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift index b1077dd..b07919e 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift @@ -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 diff --git a/AutoCat/SwiftUI/NavigationLink.swift b/AutoCat/SwiftUI/NavigationLink.swift index 9137a76..76e5c60 100644 --- a/AutoCat/SwiftUI/NavigationLink.swift +++ b/AutoCat/SwiftUI/NavigationLink.swift @@ -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)) } } diff --git a/AutoCatCore/Extensions/NullifyDate.swift b/AutoCatCore/Extensions/NullifyDate.swift index ab2fbb3..1ae5b77 100644 --- a/AutoCatCore/Extensions/NullifyDate.swift +++ b/AutoCatCore/Extensions/NullifyDate.swift @@ -9,7 +9,7 @@ import Foundation @propertyWrapper -public struct NullifyDate: Sendable { +public struct NullifyDate: Sendable, Equatable, Hashable { public var wrappedValue: Date? { didSet { diff --git a/AutoCatCore/Models/DTO/DebugInfoDto.swift b/AutoCatCore/Models/DTO/DebugInfoDto.swift index cc48571..0ef55e6 100644 --- a/AutoCatCore/Models/DTO/DebugInfoDto.swift +++ b/AutoCatCore/Models/DTO/DebugInfoDto.swift @@ -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? diff --git a/AutoCatCore/Models/DTO/OsagoDto.swift b/AutoCatCore/Models/DTO/OsagoDto.swift index 5a77483..855974d 100644 --- a/AutoCatCore/Models/DTO/OsagoDto.swift +++ b/AutoCatCore/Models/DTO/OsagoDto.swift @@ -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 = "" diff --git a/AutoCatCore/Models/DTO/VehicleBrandDto.swift b/AutoCatCore/Models/DTO/VehicleBrandDto.swift index b8817b8..d612bb4 100644 --- a/AutoCatCore/Models/DTO/VehicleBrandDto.swift +++ b/AutoCatCore/Models/DTO/VehicleBrandDto.swift @@ -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? diff --git a/AutoCatCore/Models/DTO/VehicleDto.swift b/AutoCatCore/Models/DTO/VehicleDto.swift index 2babf05..753d413 100644 --- a/AutoCatCore/Models/DTO/VehicleDto.swift +++ b/AutoCatCore/Models/DTO/VehicleDto.swift @@ -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? diff --git a/AutoCatCore/Models/DTO/VehicleEngineDto.swift b/AutoCatCore/Models/DTO/VehicleEngineDto.swift index 2e5498b..61aacc4 100644 --- a/AutoCatCore/Models/DTO/VehicleEngineDto.swift +++ b/AutoCatCore/Models/DTO/VehicleEngineDto.swift @@ -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 diff --git a/AutoCatCore/Models/DTO/VehicleEventDto.swift b/AutoCatCore/Models/DTO/VehicleEventDto.swift index fe924e7..70ebc71 100644 --- a/AutoCatCore/Models/DTO/VehicleEventDto.swift +++ b/AutoCatCore/Models/DTO/VehicleEventDto.swift @@ -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 diff --git a/AutoCatCore/Models/DTO/VehicleModelDto.swift b/AutoCatCore/Models/DTO/VehicleModelDto.swift index 299e622..94888fc 100644 --- a/AutoCatCore/Models/DTO/VehicleModelDto.swift +++ b/AutoCatCore/Models/DTO/VehicleModelDto.swift @@ -8,7 +8,7 @@ import Foundation -public struct VehicleModelDto: Decodable, Sendable, Equatable { +public struct VehicleModelDto: Decodable, Sendable, Equatable, Hashable { public var name: VehicleNameDto? } diff --git a/AutoCatCore/Models/DTO/VehicleNameDto.swift b/AutoCatCore/Models/DTO/VehicleNameDto.swift index a5e1eb5..acd5d4a 100644 --- a/AutoCatCore/Models/DTO/VehicleNameDto.swift +++ b/AutoCatCore/Models/DTO/VehicleNameDto.swift @@ -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? diff --git a/AutoCatCore/Models/DTO/VehicleNoteDto.swift b/AutoCatCore/Models/DTO/VehicleNoteDto.swift index c3e5ac6..d946fe8 100644 --- a/AutoCatCore/Models/DTO/VehicleNoteDto.swift +++ b/AutoCatCore/Models/DTO/VehicleNoteDto.swift @@ -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 = "" diff --git a/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift b/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift index 34288ff..76b5301 100644 --- a/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift +++ b/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift @@ -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 = "" diff --git a/AutoCatCore/Models/DTO/VehiclePhotoDto.swift b/AutoCatCore/Models/DTO/VehiclePhotoDto.swift index c0effd9..55f1398 100644 --- a/AutoCatCore/Models/DTO/VehiclePhotoDto.swift +++ b/AutoCatCore/Models/DTO/VehiclePhotoDto.swift @@ -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? diff --git a/AutoCatCore/Models/Filter.swift b/AutoCatCore/Models/Filter.swift index f994823..bc12a06 100644 --- a/AutoCatCore/Models/Filter.swift +++ b/AutoCatCore/Models/Filter.swift @@ -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 diff --git a/AutoCatCore/Models/VehiclesArchive.swift b/AutoCatCore/Models/VehiclesArchive.swift index 09e33cf..5bb04fa 100644 --- a/AutoCatCore/Models/VehiclesArchive.swift +++ b/AutoCatCore/Models/VehiclesArchive.swift @@ -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) + } } }