I'm trying to mimic same behaviour of the Apple Maps app search button tapped zoom out functionality but I'm not able to do it.
In the Apple Maps app, after searching a place and tapping on the search result item it shows all search result item annotations while zooming out and both shows user's current location (blue dot) and the result annotations.
In my case, I have 2 POI buttons like a tabbar item and when the user tapped in them it passes query string to MKLocalSearch and showing the results through of it. What I want is, when the tabbar item like buttons tapped I have to zoom out on the both results and the user's current location (blue dot).
struct ContentView: View {
@State private var cameraPosition: MapCameraPosition = .userLocation(followsHeading: true, fallback: .automatic)
@State private var visibleRegion: MKCoordinateRegion?
@State private var searchResults: [MKMapItem] = []
@State private var selectedResult: MKMapItem?
@State private var route: MKRoute?
@State private var selectedTabBarButton = ""
@State var isShowingBottomSheet = false
@StateObject var locationManager = LocationManager.shared
private let navigateToMaps: NavigateToMaps = .appleMaps
private let onboardingTip = OnboardingTip()
var body: some View {
if locationManager.state == .notDetermined {
LocationRequestView()
} else if locationManager.state == .denied {
LocationRequestDeniedView()
} else {
Map(position: $cameraPosition, selection: $selectedResult) {
Annotation("Current location", coordinate: .userLocation) {
}
.annotationTitles(.automatic)
ForEach(searchResults, id:\.self) {
Marker(item: $0)
}
.annotationTitles(.hidden)
if let route {
MapPolyline(route)
.stroke(.blue, lineWidth: 5)
}
}
.mapStyle(.standard(elevation: .realistic))
.safeAreaInset(edge: .bottom) {
HStack {
Spacer()
VStack(spacing: 0) {
if let selectedResult {
ItemInfoView(isShowing: $isShowingBottomSheet, route: $route, selectedTabBarButton: selectedTabBarButton, selectedResult: selectedResult, url: self.shareLocation())
.frame(height: 480)
.clipShape(RoundedRectangle(cornerRadius: 10))
.padding([.top, .horizontal])
.presentationContentInteraction(.scrolls)
}
Divider()
POIButtons(
position: $cameraPosition,
searchResults: $searchResults,
selectedTabBarButton: $selectedTabBarButton,
visibleRegion: $visibleRegion
)
.padding(.top)
}
Spacer()
}
.background(.thinMaterial)
}
.onChange(of: searchResults) {
if !searchResults.isEmpty {
visibleRegion = calculateRegionToFit(coordinates: searchResults.map { $0.placemark.coordinate })
cameraPosition = .region(visibleRegion ?? MKCoordinateRegion())
}
}
.onChange(of: selectedResult) {
getDirections()
if selectedResult?.pointOfInterestCategory == .evCharger {
selectedTabBarButton = "EV"
} else {
selectedTabBarButton = "Gas"
}
}
.mapControls {
MapUserLocationButton()
MapCompass()
MapScaleView()
}
}
}
func calculateRegionToFit(coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion? {
// First check if the coordinates array is empty
guard !coordinates.isEmpty else {
return nil
}
// Find the minimum and maximum latitude and longitude values of the search
var minLat = coordinates[0].latitude
var maxLat = coordinates[0].latitude
var minLon = coordinates[0].longitude
var maxLon = coordinates[0].longitude
for coordinate in coordinates {
minLat = min(minLat, coordinate.latitude)
maxLat = max(maxLat, coordinate.latitude)
minLon = min(minLon, coordinate.longitude)
maxLon = max(maxLon, coordinate.longitude)
}
// Calculate the region based on the search values
let center = CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2)
let span = MKCoordinateSpan(
latitudeDelta: maxLat - minLat,
longitudeDelta: maxLon - minLon
)
let region = MKCoordinateRegion(center: center, span: span)
return region
}
func getDirections() {
route = nil
isShowingBottomSheet = true
guard let selectedResult else { return }
let location = locationManager.manager.location
guard let coordinate = location?.coordinate else { return }
let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: coordinate))
request.destination = selectedResult
Task {
let directions = MKDirections(request: request)
let response = try? await directions.calculate()
route = response?.routes.first
}
}
}
I tried setting new MKCoordinateSpan values and change the cameraPosition = .region(with the new 'region')
EDIT: I added the below method calculateRegionToFit()
for solution and called it in .onChange
and updated the cameraPosition
with the new visibleRegion
of the Map like,
.onChange(of: searchResults) {
if !searchResults.isEmpty {
visibleRegion = calculateRegionToFit(
coordinates: searchResults.map { $0.placemark.coordinate })
cameraPosition = .region(visibleRegion ?? MKCoordinateRegion())
}
}
That's what I did for the solution;
Briefly, the following method directly calculates the min and max latitude and longitude values through the coordinates: [CLLocationCoordinate2D]
and then computes the center and span of the MKCoordinateRegion
using the search results (in my case search functionality is tapping EV or Gas Station tabbar buttons). I updated the usage of the calculation method in ContentView above.
func calculateRegionToFit(coordinates: [CLLocationCoordinate2D]) -> MKCoordinateRegion? {
// First check if the coordinates array is empty
guard !coordinates.isEmpty else {
return nil
}
// Find the minimum and maximum latitude and longitude values of the search
var minLat = coordinates[0].latitude
var maxLat = coordinates[0].latitude
var minLon = coordinates[0].longitude
var maxLon = coordinates[0].longitude
for coordinate in coordinates {
minLat = min(minLat, coordinate.latitude)
maxLat = max(maxLat, coordinate.latitude)
minLon = min(minLon, coordinate.longitude)
maxLon = max(maxLon, coordinate.longitude)
}
// Calculate the region based on the search values
let center = CLLocationCoordinate2D(latitude: (minLat + maxLat) / 2, longitude: (minLon + maxLon) / 2)
let span = MKCoordinateSpan(
latitudeDelta: maxLat - minLat,
longitudeDelta: maxLon - minLon
)
let region = MKCoordinateRegion(center: center, span: span)
return region
}