From 41f18b18ad3ef949743e63a31c2d4f3f2b2dcd8c Mon Sep 17 00:00:00 2001 From: Nastassia Makaranka Date: Mon, 18 Nov 2024 15:43:50 +0100 Subject: [PATCH] Improve map annotation display in the sample app (#345) * Improve the sample app point annotations * Enable Cluster point annotations * Set Map view border to not render the view overlapped by the tap bar --- MapboxSearch.xcodeproj/project.pbxproj | 16 +++++++ .../AddressAutofillResultViewController.swift | 5 +- Sources/Demo/Base.lproj/Main.storyboard | 11 +++-- Sources/Demo/DiscoverViewController.swift | 26 +++++----- .../Demo/Examples/MapsViewController.swift | 2 +- Sources/Demo/MapRootController.swift | 10 +--- .../Offline/OfflineDemoViewController.swift | 7 +-- ...aceAutocompleteDetailsViewController.swift | 11 +---- Sources/Demo/ResultDetailViewController.swift | 9 ++-- .../Demo/UIComponents/MapView+Search.swift | 15 ++++++ .../UIComponents/PointAnnotation+Search.swift | 48 +++++++++++++++++++ 11 files changed, 108 insertions(+), 52 deletions(-) create mode 100644 Sources/Demo/UIComponents/MapView+Search.swift create mode 100644 Sources/Demo/UIComponents/PointAnnotation+Search.swift diff --git a/MapboxSearch.xcodeproj/project.pbxproj b/MapboxSearch.xcodeproj/project.pbxproj index 9ecf9a5e6..94d158a5b 100644 --- a/MapboxSearch.xcodeproj/project.pbxproj +++ b/MapboxSearch.xcodeproj/project.pbxproj @@ -149,6 +149,8 @@ 2C10133429F1C6200094413F /* PlaceAutocompleteIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C10133329F1C6200094413F /* PlaceAutocompleteIntegrationTests.swift */; }; 2C18E9F429F0A83900FD96E6 /* PlaceAutocompleteSuggestionStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C18E9F229F0A82E00FD96E6 /* PlaceAutocompleteSuggestionStub.swift */; }; 2C705F062A137CEB00B8B773 /* SearchNavigationProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C705F052A137CEB00B8B773 /* SearchNavigationProfile.swift */; }; + 2C7FEBFA2CE78E6300B7ED22 /* PointAnnotation+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7FEBF92CE78E6300B7ED22 /* PointAnnotation+Search.swift */; }; + 2C7FEBFC2CE7A62C00B7ED22 /* MapView+Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C7FEBFB2CE7A62C00B7ED22 /* MapView+Search.swift */; }; 2CA1E22129F09CD200A533CF /* PlaceAutocomplete.Suggestion+Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1E22029F09CD200A533CF /* PlaceAutocomplete.Suggestion+Tests.swift */; }; 2CA1E22329F0A47600A533CF /* PlaceAutocompleteTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CA1E22229F0A47600A533CF /* PlaceAutocompleteTests.swift */; }; 2CD6C03C29F1982100D865D1 /* EventsManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CD6C03B29F1982100D865D1 /* EventsManagerTests.swift */; }; @@ -662,6 +664,8 @@ 2C10133329F1C6200094413F /* PlaceAutocompleteIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceAutocompleteIntegrationTests.swift; sourceTree = ""; }; 2C18E9F229F0A82E00FD96E6 /* PlaceAutocompleteSuggestionStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceAutocompleteSuggestionStub.swift; sourceTree = ""; }; 2C705F052A137CEB00B8B773 /* SearchNavigationProfile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchNavigationProfile.swift; sourceTree = ""; }; + 2C7FEBF92CE78E6300B7ED22 /* PointAnnotation+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PointAnnotation+Search.swift"; sourceTree = ""; }; + 2C7FEBFB2CE7A62C00B7ED22 /* MapView+Search.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MapView+Search.swift"; sourceTree = ""; }; 2CA1E22029F09CD200A533CF /* PlaceAutocomplete.Suggestion+Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PlaceAutocomplete.Suggestion+Tests.swift"; sourceTree = ""; }; 2CA1E22229F0A47600A533CF /* PlaceAutocompleteTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaceAutocompleteTests.swift; sourceTree = ""; }; 2CD6C03B29F1982100D865D1 /* EventsManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsManagerTests.swift; sourceTree = ""; }; @@ -1376,6 +1380,7 @@ 1440BF4A28FD7576009B3679 /* Search UI */ = { isa = PBXGroup; children = ( + 2C7FEBF82CE78E4F00B7ED22 /* UIComponents */, FEEDD3C62508E3FB00DC0A98 /* Resources */, FEEDD3B52508E3CD00DC0A98 /* MapRootController.swift */, 04CECA002C3EE28B007117E4 /* ResultDetailViewController.swift */, @@ -1565,6 +1570,15 @@ path = Navigation; sourceTree = ""; }; + 2C7FEBF82CE78E4F00B7ED22 /* UIComponents */ = { + isa = PBXGroup; + children = ( + 2C7FEBF92CE78E6300B7ED22 /* PointAnnotation+Search.swift */, + 2C7FEBFB2CE7A62C00B7ED22 /* MapView+Search.swift */, + ); + path = UIComponents; + sourceTree = ""; + }; 2CD6C03A29F1980700D865D1 /* Telemetry */ = { isa = PBXGroup; children = ( @@ -2934,6 +2948,7 @@ FEEDD3C32508E3CD00DC0A98 /* AppDelegate.swift in Sources */, 14F7186D29A139BF00D5BC2E /* PlaceAutocompleteDetailsViewController.swift in Sources */, 04CECA152C3EFAB9007117E4 /* TextViewLoggerViewController.swift in Sources */, + 2C7FEBFA2CE78E6300B7ED22 /* PointAnnotation+Search.swift in Sources */, 04CECA0D2C3EFAAF007117E4 /* SimpleUISearchViewController.swift in Sources */, FEEDD3BF2508E3CD00DC0A98 /* MapRootController.swift in Sources */, 04CECA162C3EFAB9007117E4 /* ExamplesListing.swift in Sources */, @@ -2942,6 +2957,7 @@ 0498A7442CB486AE008F8903 /* ForwardExampleViewController.swift in Sources */, 04CECA132C3EFAB9007117E4 /* Examples.swift in Sources */, 04CECA092C3EFAAF007117E4 /* MapboxMapsCategoryResultsViewController.swift in Sources */, + 2C7FEBFC2CE7A62C00B7ED22 /* MapView+Search.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/Demo/AddressAutofillResultViewController.swift b/Sources/Demo/AddressAutofillResultViewController.swift index 75002d35c..7f135a8ae 100644 --- a/Sources/Demo/AddressAutofillResultViewController.swift +++ b/Sources/Demo/AddressAutofillResultViewController.swift @@ -213,10 +213,7 @@ extension AddressAutofillResultViewController { func showAnnotations(results: [AddressAutofill.Result], cameraShouldFollow: Bool = true) { annotationsManager.annotations = results.compactMap { - var point = PointAnnotation(coordinate: $0.coordinate) - point.textField = $0.name - UIImage(named: "pin").map { point.image = .init(image: $0, name: "pin") } - return point + PointAnnotation.pointAnnotation($0) } if cameraShouldFollow { diff --git a/Sources/Demo/Base.lproj/Main.storyboard b/Sources/Demo/Base.lproj/Main.storyboard index 6e15490df..68730eb01 100644 --- a/Sources/Demo/Base.lproj/Main.storyboard +++ b/Sources/Demo/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -274,12 +274,13 @@ - + + @@ -401,10 +402,10 @@ - + - + diff --git a/Sources/Demo/DiscoverViewController.swift b/Sources/Demo/DiscoverViewController.swift index 685e0d79e..9c70d71ee 100644 --- a/Sources/Demo/DiscoverViewController.swift +++ b/Sources/Demo/DiscoverViewController.swift @@ -6,9 +6,10 @@ import UIKit final class DiscoverViewController: UIViewController { private var mapView = MapView(frame: .zero, mapInitOptions: defaultMapOptions) @IBOutlet private var segmentedControl: UISegmentedControl! + @IBOutlet private var searchButton: UIButton! private let category = Discover() - lazy var annotationsManager = mapView.annotations.makePointAnnotationManager() + lazy var annotationsManager = mapView.makeClusterPointAnnotationManager() override func viewDidLoad() { super.viewDidLoad() @@ -21,7 +22,7 @@ final class DiscoverViewController: UIViewController { mapView.topAnchor.constraint(equalTo: view.topAnchor), mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + mapView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) // Show user location @@ -96,11 +97,18 @@ extension DiscoverViewController { ) } else { do { + let inset: CGFloat = 24 + let insets = UIEdgeInsets( + top: inset + segmentedControl.frame.height, + left: inset, + bottom: inset + searchButton.frame.height, + right: inset + ) let cameraState = mapView.mapboxMap.cameraState let coordinatesCamera = try mapView.mapboxMap.camera( for: annotations.map(\.point.coordinates), camera: CameraOptions(cameraState: cameraState), - coordinatesPadding: UIEdgeInsets(top: 24, left: 24, bottom: 24, right: 24), + coordinatesPadding: insets, maxZoom: nil, offset: nil ) @@ -114,17 +122,7 @@ extension DiscoverViewController { private func showCategoryResults(_ results: [Discover.Result], cameraShouldFollow: Bool = true) { annotationsManager.annotations = results.map { - var point = PointAnnotation(coordinate: $0.coordinate) - point.textField = $0.name - - /// Display a corresponding Maki icon for this Result when available - if let name = $0.makiIcon, let maki = Maki(rawValue: name) { - point.image = .init(image: maki.icon, name: maki.name) - point.iconOpacity = 0.6 - point.iconAnchor = .bottom - } - - return point + PointAnnotation.pointAnnotation($0) } if cameraShouldFollow { diff --git a/Sources/Demo/Examples/MapsViewController.swift b/Sources/Demo/Examples/MapsViewController.swift index 56c2a3f37..a094c45b5 100644 --- a/Sources/Demo/Examples/MapsViewController.swift +++ b/Sources/Demo/Examples/MapsViewController.swift @@ -4,7 +4,7 @@ import UIKit class MapsViewController: UIViewController, ExampleController { let mapView = MapView(frame: .zero) - lazy var annotationsManager = mapView.annotations.makePointAnnotationManager() + lazy var annotationsManager = mapView.makeClusterPointAnnotationManager() override func viewDidLoad() { super.viewDidLoad() diff --git a/Sources/Demo/MapRootController.swift b/Sources/Demo/MapRootController.swift index c90f9e452..760c0a440 100644 --- a/Sources/Demo/MapRootController.swift +++ b/Sources/Demo/MapRootController.swift @@ -19,7 +19,7 @@ class MapRootController: UIViewController { mapView.topAnchor.constraint(equalTo: view.topAnchor), mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor), mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor), - mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + mapView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), ]) // Show user location @@ -48,13 +48,7 @@ class MapRootController: UIViewController { func showAnnotations(results: [SearchResult], cameraShouldFollow: Bool = true) { annotationsManager.annotations = results.map { result in - var point = PointAnnotation(coordinate: result.coordinate) - point.textField = result.name - UIImage(named: "pin").map { - point.iconAnchor = .bottom - point.textAnchor = .top - point.image = .init(image: $0, name: "pin") - } + var point = PointAnnotation.pointAnnotation(result) // Present a detail view upon annotation tap point.tapHandler = { [weak self] _ in diff --git a/Sources/Demo/Offline/OfflineDemoViewController.swift b/Sources/Demo/Offline/OfflineDemoViewController.swift index 1e4092d5f..001528d7d 100644 --- a/Sources/Demo/Offline/OfflineDemoViewController.swift +++ b/Sources/Demo/Offline/OfflineDemoViewController.swift @@ -7,7 +7,7 @@ import UIKit /// Demonstrate how to use Offline Search in the Demo app class OfflineDemoViewController: UIViewController { private var mapView = MapView(frame: .zero) - lazy var annotationsManager = mapView.annotations.makePointAnnotationManager() + lazy var annotationsManager = mapView.makeClusterPointAnnotationManager() private var messageLabel = UILabel() private lazy var searchController = MapboxSearchController() @@ -103,10 +103,7 @@ class OfflineDemoViewController: UIViewController { func showAnnotations(results: [SearchResult], cameraShouldFollow: Bool = true) { annotationsManager.annotations = results.map { - var point = PointAnnotation(coordinate: $0.coordinate) - point.textField = $0.name - UIImage(named: "pin").map { point.image = .init(image: $0, name: "pin") } - return point + PointAnnotation.pointAnnotation($0) } if cameraShouldFollow { diff --git a/Sources/Demo/PlaceAutocompleteDetailsViewController.swift b/Sources/Demo/PlaceAutocompleteDetailsViewController.swift index 9341f0dc2..67b195fd7 100644 --- a/Sources/Demo/PlaceAutocompleteDetailsViewController.swift +++ b/Sources/Demo/PlaceAutocompleteDetailsViewController.swift @@ -6,7 +6,7 @@ import UIKit final class PlaceAutocompleteResultViewController: UIViewController { @IBOutlet private var tableView: UITableView! @IBOutlet private var mapView: MapView! - lazy var annotationsManager = mapView.annotations.makePointAnnotationManager() + lazy var annotationsManager = mapView.makeClusterPointAnnotationManager() private var result: PlaceAutocomplete.Result! private var resultComponents: [(name: String, value: String)] = [] @@ -39,14 +39,7 @@ final class PlaceAutocompleteResultViewController: UIViewController { func showAnnotations(results: [PlaceAutocomplete.Result], cameraShouldFollow: Bool = true) { annotationsManager.annotations = results.compactMap { - guard let coordinate = $0.coordinate else { - return nil - } - - var point = PointAnnotation(coordinate: coordinate) - point.textField = $0.name - UIImage(named: "pin").map { point.image = .init(image: $0, name: "pin") } - return point + PointAnnotation.pointAnnotation($0) } if cameraShouldFollow { diff --git a/Sources/Demo/ResultDetailViewController.swift b/Sources/Demo/ResultDetailViewController.swift index f88f223ae..c94e54df6 100644 --- a/Sources/Demo/ResultDetailViewController.swift +++ b/Sources/Demo/ResultDetailViewController.swift @@ -58,12 +58,9 @@ class ResultDetailViewController: UIViewController { ]) /// Add annotations and set camera - let annotationsManager = mapView.annotations.makePointAnnotationManager() - annotationsManager.annotations = [result].map { result in - var point = PointAnnotation(coordinate: result.coordinate) - point.textField = result.name - UIImage(named: "pin").map { point.image = .init(image: $0, name: "pin") } - return point + let annotationsManager = mapView.makeClusterPointAnnotationManager() + annotationsManager.annotations = [result].map { + PointAnnotation.pointAnnotation($0) } if let annotation = annotationsManager.annotations.first { diff --git a/Sources/Demo/UIComponents/MapView+Search.swift b/Sources/Demo/UIComponents/MapView+Search.swift new file mode 100644 index 000000000..3e6da95f4 --- /dev/null +++ b/Sources/Demo/UIComponents/MapView+Search.swift @@ -0,0 +1,15 @@ +import MapboxMaps +import UIKit + +extension MapView { + func makeClusterPointAnnotationManager( + duration: TimeInterval = 0.5 + ) -> PointAnnotationManager { + let manager = annotations.makePointAnnotationManager(clusterOptions: .init()) + manager.onClusterTap = { [weak self] context in + let cameraOptions = CameraOptions(center: context.coordinate, zoom: context.expansionZoom) + self?.camera.ease(to: cameraOptions, duration: duration) + } + return manager + } +} diff --git a/Sources/Demo/UIComponents/PointAnnotation+Search.swift b/Sources/Demo/UIComponents/PointAnnotation+Search.swift new file mode 100644 index 000000000..06f0eb03b --- /dev/null +++ b/Sources/Demo/UIComponents/PointAnnotation+Search.swift @@ -0,0 +1,48 @@ +import MapboxMaps +import MapboxSearch +import UIKit + +extension PointAnnotation { + static func pointAnnotation(_ searchResult: SearchResult) -> Self { + Self.pointAnnotation(coordinate: searchResult.coordinate, name: searchResult.name) + } + + static func pointAnnotation(_ searchResult: AddressAutofill.Result) -> Self { + Self.pointAnnotation(coordinate: searchResult.coordinate, name: searchResult.name) + } + + static func pointAnnotation(_ searchResult: PlaceAutocomplete.Result) -> Self? { + guard let coordinate = searchResult.coordinate else { return nil } + return Self.pointAnnotation(coordinate: coordinate, name: searchResult.name) + } + + static func pointAnnotation(_ searchResult: Discover.Result) -> Self { + var point = Self.pointAnnotation(coordinate: searchResult.coordinate, name: searchResult.name, imageName: nil) + + /// Display a corresponding Maki icon for this Result when available + if let name = searchResult.makiIcon, let maki = Maki(rawValue: name) { + point.image = .init(image: maki.icon, name: maki.name) + point.iconOpacity = 0.6 + point.iconAnchor = .bottom + point.textAnchor = .top + } + return point + } + + static func pointAnnotation( + coordinate: CLLocationCoordinate2D, + name: String, + imageName: String? = "pin" + ) -> Self { + var point = PointAnnotation(coordinate: coordinate) + point.textField = name + point.textHaloColor = .init(.white) + point.textHaloWidth = 10 + if let imageName, let image = UIImage(named: "pin") { + point.iconAnchor = .bottom + point.textAnchor = .top + point.image = .init(image: image, name: "pin") + } + return point + } +}