Search code examples
swiftuimapkitios17

iOS 17 MapKit SwiftUI - Zoom out when the button selected


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())
             }
        }

Solution

  • 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
    }