Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SwiftUI Map Change Region Programmatically to Show Any Annotations Added

I'm trying to use the new Map in SwiftUI. I would like to change the visible region programmatically to include all of the annotations that are added programmatically, similar to the way you can show a bounding box for a route overlay in the older UIMap. I have not found anything in the documentation to do this. I created my own approach by getting the min/max values for the latitude and longitude of each annotation and then creating a center for the new region and a span. Both of these involve some math. My result works for my North America location, but I will need to add significant complexity to handle the cases where the annotation area encompasses the equator, the date line or the prime meridian. Before I do so, I'm hoping someone has a solution that I have missed.

Here is my solution:

struct MyMapView: View {
    @StateObject var myMapVM = MyMapViewModel()

    var body: some View {
        VStack {
            let m = Map(coordinateRegion: $myMapVM.region, annotationItems: myMapVM.centers) { site in
            
                MapAnnotation(coordinate: CLLocationCoordinate2D(latitude: site.lat, longitude: site.long)) {
                    GroupAnnotationPinView(title: site.name)
                        .onTapGesture {
                            openMapWithCoordinate(coordinate: CLLocationCoordinate2D(latitude: site.lat, longitude: site.long), name: site.name)
                        }//on tap
                }//map annotation
            }//map
            m.onAppear {
                //this works, so reference to Map seems to work ok
                //print(m.body)
            }
        
            Button(action: {
                myMapVM.createCoordinateRegion()
            }, label: {
                Image(systemName: "square.and.pencil")
                    .resizable()
                    .frame(width: 44, height: 44)
                    .padding()
            })
                .padding(.bottom, 20)
        }//v
        .ignoresSafeArea()
    }//body

    func openMapWithCoordinate(coordinate: CLLocationCoordinate2D, name: String) {
        let place = MKPlacemark(coordinate: coordinate)
        let mapItem = MKMapItem(placemark: place)
        mapItem.name = name
        mapItem.openInMaps(launchOptions: nil)
    }//open
}//my map view

struct GroupAnnotationPinView: View {
    @State private var showTitle = true
    let title: String
    var body: some View {
        VStack(spacing: 0) {
            Image(systemName: "mappin")
                .font(.title)
                .foregroundColor(.red)
        }//v
    }//body
}//group anno view

And the View Model

class MyMapViewModel: ObservableObject {

    @Published var annotations: [MKAnnotation] = []
    @Published var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(latitude: 37.334_900,
                                       longitude: -122.009_020),
        latitudinalMeters: 10000,
        longitudinalMeters: 10000
    )

    let centers: [Center] = [
        .init(name: "One", lat: 37.334, long: -122.009),
        .init(name: "Two", lat: 37.380, long: -122.010),
        .init(name: "Three", lat: 37.400, long: -122.010),
        .init(name: "Four", lat: 40.000, long: -120.000)
            //.init(name: "Four", lat: 37.600, long: -121.800)
        ]

    func createCoordinateRegion() {
    
        //you need to fix this to account for dateline, prime meridian and equator in span
    
        let maxX = centers.max(\.lat)
        let maxY = centers.max(\.long)
        let minX = centers.min(\.lat)
        let minY = centers.min(\.long)
        //print("minX.lat is ", minX?.lat ?? "nil")
        //print("maxX.lat is ", maxX?.lat ?? "nil")
        //print("minY.long is ", minY?.long ?? "nil")
        //print("maxY.long is ", maxY?.long ?? "nil")
    
        guard let minXS = minX?.lat, let maxXS = maxX?.lat, let minYS = minY?.long, let maxYS = maxY?.long else { return }
        
        let deltaX = maxXS - minXS
        let deltaY = maxYS - minYS
    
        let newCenterLat = minXS + deltaX / 2
        let newCenterLong = maxYS - abs(deltaY / 2)
        //print(newCenterLat)
        //print(newCenterLong)
    
        let newRegion = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: newCenterLat, longitude: newCenterLong), span: MKCoordinateSpan(latitudeDelta: max(deltaX, deltaY), longitudeDelta: max(deltaX, deltaY)))
    
        region = newRegion

    }//create coord region


}//class

extension Sequence {
    func max<T: Comparable>(_ predicate: (Element) -> T)  -> Element? {
        self.max(by: { predicate($0) < predicate($1) })
    }
    func min<T: Comparable>(_ predicate: (Element) -> T)  -> Element? {
        self.min(by: { predicate($0) < predicate($1) })
    }
}// ext seq

struct Center: Identifiable, Hashable {
    let id = UUID()
    let name: String
    let lat: Double
    let long: Double
}//center

Any guidance would be appreciated: Xcode 13.2.1 iOS 15.2

like image 263
JohnSF Avatar asked Oct 15 '25 04:10

JohnSF


1 Answers

I do this by converting each annotation coordinate to an MKMapRect:

let rect = MKMapRect(origin: MKMapPoint(coordinate), size: MKMapSize(width: 0, height: 0))

MKMapRect is able to calculate a union and knows how to handle the 180th Meridian (see method spans180thMeridian:

private(set) var union: MKMapRect? = nil
func union(_ rect: MKMapRect) {
    guard let union = self.union else {
        self.union = rect
        return
    }
    self.union = union.union(rect)
}

After you're done, you can convert it into a region and use it:

let region = MKCoordinateRegion(union)

Be aware that this cuts the AnnotationViews at the edge in half, so you have to calculate some space around it.

Alternative solution: This is one of MANY reasons to prefer the old MKMapKit where you just call

mapView.showAnnotations(specificAnnotations, animated: true)

This changes the region to show specificAnnotations while keeping the other annotations on the map and not cutting AnnotationViews in half.

like image 56
Gerd Castan Avatar answered Oct 18 '25 02:10

Gerd Castan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!