diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 77ea4cc..ad657b9 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -46,6 +46,12 @@ 7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */; }; 7A2E6FA72C42B3AD00C40DA7 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; }; 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 */; }; @@ -185,10 +191,8 @@ 7ACBB91E2CB9B155005A5168 /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7ACBB91D2CB9B155005A5168 /* Mockable */; }; 7ACBB9202CB9B16C005A5168 /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7ACBB91F2CB9B16C005A5168 /* Mockable */; }; 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C92250B954900F237B2 /* Navigation.swift */; }; - 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C94250D037700F237B2 /* ShowEventController.swift */; }; 7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */; }; 7ADF6C99250F872C00F237B2 /* RoadNumbers.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */; }; - 7ADF6C9F251201D200F237B2 /* GlobalEventsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C9E251201D200F237B2 /* GlobalEventsController.swift */; }; 7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6CA02512244400F237B2 /* MapExt.swift */; }; 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE24C5E251F1B4E00758E39 /* Buttons.swift */; }; 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; }; @@ -325,6 +329,10 @@ 7A2E6FA32C42B3AD00C40DA7 /* AutoCatCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; 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 = ""; }; @@ -467,10 +475,8 @@ 7AC76D7A270083AE0084DB27 /* TextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextView.swift; sourceTree = ""; }; 7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISearchTextField+Dumb.swift"; sourceTree = ""; }; 7ADF6C92250B954900F237B2 /* Navigation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Navigation.swift; sourceTree = ""; }; - 7ADF6C94250D037700F237B2 /* ShowEventController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowEventController.swift; sourceTree = ""; }; 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNKeyboard.swift; sourceTree = ""; }; 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers.otf; sourceTree = ""; }; - 7ADF6C9E251201D200F237B2 /* GlobalEventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventsController.swift; sourceTree = ""; }; 7ADF6CA02512244400F237B2 /* MapExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapExt.swift; sourceTree = ""; }; 7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; @@ -515,7 +521,9 @@ files = ( 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */, 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */, + 7A386A4B2DAC35F10051676A /* ClusterMap in Frameworks */, 7A7AA2CA2DA2C85100276D83 /* RealmSwift in Frameworks */, + 7A386A4D2DAC35F10051676A /* ClusterMapSwiftUI in Frameworks */, 7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */, 7ACBB91E2CB9B155005A5168 /* Mockable in Frameworks */, 7AF6D2042677C03B0086EA64 /* AutoCatCore.framework in Frameworks */, @@ -647,7 +655,6 @@ 7A11471423FDEAF800B424AF /* Controllers */ = { isa = PBXGroup; children = ( - 7A813DC7250B5C6E00CC93B9 /* Location */, 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */, 7A11471523FDEB2A00B424AF /* MainSplitController.swift */, 7AC3554B29696A1C00889457 /* MainTabController.swift */, @@ -718,6 +725,7 @@ 7A1441632C297E9800E79018 /* Screens */ = { isa = PBXGroup; children = ( + 7A386A3E2DABDBFF0051676A /* MapScreen */, 7AF231912DA1C26C00AE5EB3 /* AuthScreen */, 7A95197E2D80B69800E69883 /* RecordsScreen */, 7A5911EC2D63225500EC51BA /* SearchScreen */, @@ -766,6 +774,17 @@ path = Data; sourceTree = ""; }; + 7A386A3E2DABDBFF0051676A /* MapScreen */ = { + isa = PBXGroup; + children = ( + 7A386A3F2DABDC190051676A /* MapScreen.swift */, + 7A386A432DABDC360051676A /* MapViewModel.swift */, + 7A386A452DABDC660051676A /* MapCoordinator.swift */, + 7A386A472DABE0D00051676A /* MapMarkerModel.swift */, + ); + path = MapScreen; + sourceTree = ""; + }; 7A3F07A924360D9100E59687 /* Extensions */ = { isa = PBXGroup; children = ( @@ -963,15 +982,6 @@ path = ThirdParty; sourceTree = ""; }; - 7A813DC7250B5C6E00CC93B9 /* Location */ = { - isa = PBXGroup; - children = ( - 7ADF6C94250D037700F237B2 /* ShowEventController.swift */, - 7ADF6C9E251201D200F237B2 /* GlobalEventsController.swift */, - ); - path = Location; - sourceTree = ""; - }; 7A9519772D80B3B200E69883 /* AudioRecordService */ = { isa = PBXGroup; children = ( @@ -1238,6 +1248,8 @@ 7AC355492969652F00889457 /* SwiftEntryKit */, 7ACBB91D2CB9B155005A5168 /* Mockable */, 7A7AA2C92DA2C85100276D83 /* RealmSwift */, + 7A386A4A2DAC35F10051676A /* ClusterMap */, + 7A386A4C2DAC35F10051676A /* ClusterMapSwiftUI */, ); productName = AutoCat; productReference = 7A1146FD23FDE7E500B424AF /* AutoCat.app */; @@ -1360,6 +1372,7 @@ 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */, 7A6C4D9C2C56BCA600982597 /* XCRemoteSwiftPackageReference "SwiftLocation" */, 7ACBB91C2CB9B155005A5168 /* XCRemoteSwiftPackageReference "Mockable" */, + 7A386A492DAC35F10051676A /* XCRemoteSwiftPackageReference "ClusterMap" */, ); productRefGroup = 7A1146FE23FDE7E500B424AF /* Products */; projectDirPath = ""; @@ -1440,9 +1453,12 @@ 7AC3555029696D5A00889457 /* NewNumberController.swift in Sources */, 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 */, @@ -1466,6 +1482,7 @@ 7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */, 7A7158072C44085600852088 /* OsagoScreen.swift in Sources */, 7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */, + 7A386A442DABDC360051676A /* MapViewModel.swift in Sources */, 7AAAFAD32C4D0FD00050410D /* ACImageSliderView.swift in Sources */, 7A912F372D381B7400002938 /* LicensePlateView.swift in Sources */, 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */, @@ -1483,7 +1500,6 @@ 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */, 7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */, 7A1022722C554A1300B84627 /* CustomHostingController.swift in Sources */, - 7ADF6C9F251201D200F237B2 /* GlobalEventsController.swift in Sources */, 7A1022792C557ED600B84627 /* LocationPickerViewModel.swift in Sources */, 7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */, 7AF231932DA1C28100AE5EB3 /* AuthScreen.swift in Sources */, @@ -1510,7 +1526,6 @@ 7A4955822D58CCF900912E66 /* HistoryFilter.swift in Sources */, 7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */, 7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */, - 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */, 7A71580C2C44453200852088 /* AdsScreen.swift in Sources */, 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */, 7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */, @@ -1822,7 +1837,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 155; + CURRENT_PROJECT_VERSION = 156; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; @@ -1849,7 +1864,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 155; + CURRENT_PROJECT_VERSION = 156; DEVELOPMENT_TEAM = 46DTTB8X4S; INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; @@ -2106,6 +2121,14 @@ minimumVersion = 10.36.0; }; }; + 7A386A492DAC35F10051676A /* XCRemoteSwiftPackageReference "ClusterMap" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/vospennikov/ClusterMap.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.1.1; + }; + }; 7A6C4D9C2C56BCA600982597 /* XCRemoteSwiftPackageReference "SwiftLocation" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/malcommac/SwiftLocation.git"; @@ -2149,6 +2172,16 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 7A386A4A2DAC35F10051676A /* ClusterMap */ = { + isa = XCSwiftPackageProductDependency; + package = 7A386A492DAC35F10051676A /* XCRemoteSwiftPackageReference "ClusterMap" */; + productName = ClusterMap; + }; + 7A386A4C2DAC35F10051676A /* ClusterMapSwiftUI */ = { + isa = XCSwiftPackageProductDependency; + package = 7A386A492DAC35F10051676A /* XCRemoteSwiftPackageReference "ClusterMap" */; + productName = ClusterMapSwiftUI; + }; 7A6C4D9D2C56BCA600982597 /* SwiftLocation */ = { isa = XCSwiftPackageProductDependency; package = 7A6C4D9C2C56BCA600982597 /* XCRemoteSwiftPackageReference "SwiftLocation" */; diff --git a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 027891f..86269e4 100644 --- a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,15 @@ { - "originHash" : "6fccb9fdc0d29647d4f0b927aef60f375302d72b5b724992eab52ac0d8ec71c3", + "originHash" : "9a7838ec160dde976fc2028176dc43038769553e6d8eb4d7d117f851f899baaf", "pins" : [ + { + "identity" : "clustermap", + "kind" : "remoteSourceControl", + "location" : "https://github.com/vospennikov/ClusterMap.git", + "state" : { + "revision" : "f3e60a742e6d3c149ce5d0c7424f373ef1b38140", + "version" : "2.1.1" + } + }, { "identity" : "exceptioncatcher", "kind" : "remoteSourceControl", diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index 949abb1..e0948fe 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -55,7 +55,7 @@ - + @@ -69,7 +69,7 @@ - + @@ -83,62 +83,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/AutoCat/Controllers/Location/GlobalEventsController.swift b/AutoCat/Controllers/Location/GlobalEventsController.swift deleted file mode 100644 index 210b82b..0000000 --- a/AutoCat/Controllers/Location/GlobalEventsController.swift +++ /dev/null @@ -1,88 +0,0 @@ -import UIKit -import MapKit -import PKHUD -import AutoCatCore - -class EventPin: NSObject, MKAnnotation { - var coordinate: CLLocationCoordinate2D - var title: String? - var subtitle: String? - var id: String - - init(id: String, coordinate: CLLocationCoordinate2D, title: String?, subtitle: String) { - self.coordinate = coordinate - self.title = title - self.subtitle = subtitle - self.id = id - } - - convenience init(event: VehicleEventDto) { - let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude) - let address = event.address ?? "\(event.latitude), \(event.longitude)" - let date = Date(timeIntervalSince1970: event.date) - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .short - let dateStr = formatter.string(from: date) - - if let number = event.number { - self.init(id: event.id, coordinate: coordinate, title: number, subtitle: dateStr) - } else { - self.init(id: event.id, coordinate: coordinate, title: dateStr, subtitle: address) - } - } -} - -class GlobalEventsController: UIViewController { - - var map: MKMapView! - - var filter: Filter! - - let apiService: ApiServiceProtocol = ServiceContainer.shared.resolve(ApiServiceProtocol.self) - - override func viewDidLoad() { - super.viewDidLoad() - - map = MKMapView() - map.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(map) - - NSLayoutConstraint.activate([ - map.leadingAnchor.constraint(equalTo: view.leadingAnchor), - map.trailingAnchor.constraint(equalTo: view.trailingAnchor), - map.topAnchor.constraint(equalTo: view.topAnchor), - map.bottomAnchor.constraint(equalTo: view.bottomAnchor) - ]) - - #if targetEnvironment(macCatalyst) - - if #available(OSX 11.0, *) { - self.map.showsCompass = true - } - self.map.showsZoomControls = true - - #endif - - Task { await loadEvents() } - } - - func loadEvents() async { - do { - HUD.show(.progress) - let events = try await apiService.events(with: self.filter) - self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count) - let pins = events.map(EventPin.init(event:)) - self.map.removeAnnotations(self.map.annotations) - self.map.addAnnotations(pins) - self.map.centerOnPins() - HUD.hide() - } catch { - HUD.show(error: error) - } - } - - @IBAction func close(_ sender: UIBarButtonItem) { - self.dismiss(animated: true, completion: nil) - } -} diff --git a/AutoCat/Controllers/Location/ShowEventController.swift b/AutoCat/Controllers/Location/ShowEventController.swift deleted file mode 100644 index f1063c7..0000000 --- a/AutoCat/Controllers/Location/ShowEventController.swift +++ /dev/null @@ -1,28 +0,0 @@ -import UIKit -import MapKit -import AutoCatCore - -class ShowEventController: UIViewController { - - private var map = MKMapView() - var event: VehicleEventDto? - - override func viewDidLoad() { - super.viewDidLoad() - - self.map.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(self.map) - self.map.leadingAnchor.constraint(equalTo: self.view.leadingAnchor).isActive = true - self.map.trailingAnchor.constraint(equalTo: self.view.trailingAnchor).isActive = true - self.map.topAnchor.constraint(equalTo: self.view.topAnchor).isActive = true - self.map.bottomAnchor.constraint(equalTo: self.view.bottomAnchor).isActive = true - - if let event = self.event { - self.title = event.address ?? String(format: "%.02f, %.02f", event.latitude, event.longitude) - let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude), latitudinalMeters: 1000, longitudinalMeters: 1000) - self.map.setRegion(region, animated: true) - let pin = EventPin(event: event) - self.map.addAnnotation(pin) - } - } -} diff --git a/AutoCat/Screens/MapScreen/MapCoordinator.swift b/AutoCat/Screens/MapScreen/MapCoordinator.swift new file mode 100644 index 0000000..27fe78d --- /dev/null +++ b/AutoCat/Screens/MapScreen/MapCoordinator.swift @@ -0,0 +1,45 @@ +// +// 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/MapMarkerModel.swift b/AutoCat/Screens/MapScreen/MapMarkerModel.swift new file mode 100644 index 0000000..7702583 --- /dev/null +++ b/AutoCat/Screens/MapScreen/MapMarkerModel.swift @@ -0,0 +1,17 @@ +// +// MapMarkerModel.swift +// AutoCat +// +// Created by Selim Mustafaev on 13.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import CoreLocation +import ClusterMap + +struct MapMarkerModel: CoordinateIdentifiable, Identifiable, Hashable { + + var id = UUID() + var title: String + var coordinate: CLLocationCoordinate2D +} diff --git a/AutoCat/Screens/MapScreen/MapScreen.swift b/AutoCat/Screens/MapScreen/MapScreen.swift new file mode 100644 index 0000000..f8b74ba --- /dev/null +++ b/AutoCat/Screens/MapScreen/MapScreen.swift @@ -0,0 +1,43 @@ +// +// MapScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 13.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import MapKit +import ClusterMapSwiftUI + +struct MapScreen: View { + + @State var viewModel: MapViewModel + + var body: some View { + Map { + ForEach(viewModel.markers) { + Marker($0.title, coordinate: $0.coordinate) + } + ForEach(viewModel.clusters) { + Marker($0.title, coordinate: $0.coordinate) + } + } + .onAppear { + Task { await viewModel.onAppear() } + } + .hud($viewModel.hud) + .navigationTitle(viewModel.title) + .readSize { newValue in + viewModel.mapSize = newValue + } + .onMapCameraChange { context in + viewModel.currentRegion = context.region + } + .onMapCameraChange(frequency: .onEnd) { context in + Task.detached { + await viewModel.reloadMarkers() + } + } + } +} diff --git a/AutoCat/Screens/MapScreen/MapViewModel.swift b/AutoCat/Screens/MapScreen/MapViewModel.swift new file mode 100644 index 0000000..48585f1 --- /dev/null +++ b/AutoCat/Screens/MapScreen/MapViewModel.swift @@ -0,0 +1,100 @@ +// +// MapViewModel.swift +// AutoCat +// +// Created by Selim Mustafaev on 13.04.2025. +// Copyright © 2025 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore +import CoreLocation +import MapKit +import ClusterMap + +@MainActor +@Observable +final class MapViewModel: ACHudContainer { + + let apiService: ApiServiceProtocol + let mapInput: MapInput + + var hud: ACHud? + + var title: String = "" + var markers: [MapMarkerModel] = [] + var clusters: [MapMarkerModel] = [] + + let clusterManager = ClusterManager() + var mapSize: CGSize = .zero + var currentRegion: MKCoordinateRegion = .init() + + init(apiService: ApiServiceProtocol, mapInput: MapInput) { + + self.apiService = apiService + self.mapInput = mapInput + } + + func onAppear() async { + + switch mapInput { + case .event(let event): + markers = [makeMarkerModel(from: event)] + case .filter(let filter): + await loadEvents(with: filter) + } + } + + func makeMarkerModel(from event: VehicleEventDto) -> MapMarkerModel { + + let date = Date(timeIntervalSince1970: event.date) + let dateString = Formatters.marker.string(from: date) + let coordinate = CLLocationCoordinate2DMake(event.latitude, event.longitude) + + return .init( + title: dateString, + coordinate: coordinate + ) + } + + func loadEvents(with filter: Filter) async { + await wrapWithToast { [weak self] in + guard let self else { return } + let events = try await apiService.events(with: filter) + let markers = events.map(makeMarkerModel) + await clusterManager.add(markers) + await reloadMarkers() + } + } + + nonisolated func reloadMarkers() async { + async let changes = clusterManager.reload( + mapViewSize: mapSize, + coordinateRegion: currentRegion + ) + await applyChanges(changes) + } + + private func applyChanges(_ difference: ClusterManager.Difference) { + for removal in difference.removals { + switch removal { + case .annotation(let annotation): + markers.removeAll { $0 == annotation } + case .cluster(let clusterAnnotation): + clusters.removeAll { $0.id == clusterAnnotation.id } + } + } + for insertion in difference.insertions { + switch insertion { + case .annotation(let newItem): + markers.append(newItem) + case .cluster(let newItem): + clusters.append(MapMarkerModel( + id: newItem.id, + title: String(newItem.memberAnnotations.count), + coordinate: newItem.coordinate + )) + } + } + } +} diff --git a/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift b/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift index 5d49e83..23e0624 100644 --- a/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift +++ b/AutoCat/Screens/RecordsScreen/RecordsCoordinator.swift @@ -37,9 +37,7 @@ final class RecordsCoordinator { func showOnMap(event: VehicleEventDto) { - let controller = ShowEventController() - controller.event = event - controller.hidesBottomBarWhenPushed = true - navController.pushViewController(controller, animated: true) + let coordinator = MapCoordinator(navController: navController) + coordinator.start(mapInput: .event(event)) } } diff --git a/AutoCat/Screens/SearchScreen/SearchCoordinator.swift b/AutoCat/Screens/SearchScreen/SearchCoordinator.swift index 3fbcddc..7b5ac0c 100644 --- a/AutoCat/Screens/SearchScreen/SearchCoordinator.swift +++ b/AutoCat/Screens/SearchScreen/SearchCoordinator.swift @@ -48,10 +48,8 @@ final class SearchCoordinator { func showOnMap(filter: Filter) { - let controller = GlobalEventsController() - controller.filter = filter - controller.modalPresentationStyle = .fullScreen - navController.pushViewController(controller, animated: true) + let coordinator = MapCoordinator(navController: navController) + coordinator.start(mapInput: .filter(filter)) } func export(url: URL) {