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
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With