swiftUI Mapkit and Corelocation causing problems - swiftui

I'm reposting my question of yesterday and now adding a clean code example to demonstrate the problem
I have a MyCustomMapView, embedding a MKMApView and it starts at a fixed location. I have a function called gotoCoordinate, which accepts a coordinate and then navigates the mapview's center to that coordinate.
In the sample code that can be simulated by clicking on the red button labelleing "Click here to change map position".
This all works great. Until....
in the app I'm working on I also need to have a user location so I have a LocationViewModel handling the request. Once you have given request to access your location, click the button no longer moves the center of the map to that new coordinate.
Once you comment the #StateObject var locationViewModel = LocationViewModel() it is working again.
So it seems that once you are using a location manager with a delegate the map no longer moves when changing it's region
Is this a bug or am I doing something wrong?
import SwiftUI
struct ContentView: View {
#StateObject var locationViewModel = LocationViewModel()
var body: some View {
switch locationViewModel.authorizationStatus {
case .notDetermined:
AnyView(RequestLocationView())
.environmentObject(locationViewModel)
case .restricted:
ErrorView(errorText: "Location use is restricted.")
case .denied:
ErrorView(errorText: "The app does not have location permissions. Please enable them in settings.")
default:
EmptyView()
}
GeometryReader { geometry in
DisplayMapView(size:geometry.size)
}
}
}
import SwiftUI
import CoreLocation
import MapKit
struct MyCustomMapView: UIViewRepresentable {
var map = MKMapView() // << constructor contract !!
let coordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:31,longitude: -86 )
func makeUIView(context: Context) -> MKMapView {
map.delegate = context.coordinator
map.showsUserLocation = true
map.showsCompass = true
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: coordinate.latitude,longitude: coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
map.setRegion(region, animated: true)
return map
}
func gotoCoordinate(_ newCoordinate: CLLocationCoordinate2D ){
let region = MKCoordinateRegion(center: newCoordinate, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
map.setRegion(region, animated: true)
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
func makeCoordinator() -> MyCustomMapView.Coordinator {
return MyCustomMapView.Coordinator(parent1: self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var parent:MyCustomMapView
init(parent1:MyCustomMapView){
parent = parent1
}
}//class Coordinator
}
import SwiftUI
import CoreLocation
import MapKit
struct DisplayMapView: View {
#Environment(\.presentationMode) var presentationMode
var size: CGSize
var startCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
var map = MyCustomMapView()
var body: some View {
ZStack(alignment:.top){
map
VStack(alignment:.leading){
HStack {
HStack {
Text("Click here to change map position")
.onTapGesture(){
map.gotoCoordinate(startCoordinate)
}
}
.padding(EdgeInsets(top: 8, leading: 6, bottom: 8, trailing: 6))
.foregroundColor(.black)
.background(Color(.red))
.cornerRadius(10.0)
}
}.padding(.top,50).padding(.leading,20).padding(.trailing,20)
}.ignoresSafeArea()
}
}
import Foundation
import SwiftUI
import CoreLocation
class LocationViewModel: NSObject, ObservableObject, CLLocationManagerDelegate {
#Published var authorizationStatus: CLAuthorizationStatus
#Published var lastSeenLocation: CLLocation?
#Published var currentPlacemark: CLPlacemark?
private let locationManager: CLLocationManager
static let shared = LocationViewModel()
override init() {
locationManager = CLLocationManager()
authorizationStatus = locationManager.authorizationStatus
super.init()
locationManager.delegate = self
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.distanceFilter = 0.4
locationManager.startUpdatingLocation()
}
func requestPermission() {
locationManager.requestWhenInUseAuthorization()
}
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
authorizationStatus = manager.authorizationStatus
}
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
lastSeenLocation = locations.first
}
}
struct RequestLocationView: View {
#EnvironmentObject var locationViewModel: LocationViewModel
var body: some View {
VStack(spacing:50) {
Image(systemName: "location.circle")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
.foregroundColor(Color.init(red: 0.258, green: 0.442, blue: 0.254))
Button(action: {
locationViewModel.requestPermission()
}, label: {
Label(LocalizedStringKey("allowLocationAccess"), systemImage: "location")
})
.padding(10)
.foregroundColor(.white)
.background(.green)
.clipShape(RoundedRectangle(cornerRadius: 8))
Text("We need your permission to give you the best experience.")
.foregroundColor(.gray)
.font(.caption)
}
}
}
struct ErrorView: View {
var errorText: String
var body: some View {
VStack {
Image(systemName: "xmark.octagon")
.resizable()
.frame(width: 100, height: 100, alignment: .center)
Text(errorText)
}
.padding()
.foregroundColor(.white)
.background(Color.red)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}

Declare your coordinates as a stateful variable, either as #State or as #Published within an observable object:
struct DisplayMapView: View {
#State var coordinates = CLLocationCoordinate2D(latitude:40.741895,longitude: -73.989308)
Then pass the coordinates in as an argument to your view - no need to store your view as a variable:
ZStack(alignment: .top) {
MyMapView(coordinates: coordinates)
VStack(alignment: .leading) {
// etc.
Then you’ll need to do some rejigging in your UIViewRepresentable. You mustn't retain map as a separate instance outside makeUIView and updateUIView - SwiftUI structs can be recreated at will, so that would release your MKMapView instance and create a new one. Instead, the object returned by makeUIView is retained for you by the system. You do need to declare a variable that will accept the coordinates argument above, and then respond to any changes in it in updateUIView.
struct MyMapView: UIViewRepresentable {
var coordinates: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
// etc.
return map
}
func updateUIView(_ uiView: MKMapView, context: Coordinator) {
let region = MKCoordinateRegion(center: coordinates, span: MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2))
uiView.setRegion(region, animated: true)
}
}
Now, when the user taps, instead of calling a function inside your view, you update the DisplayMapView’s coordinates variable and the UIViewRepresentable’s update logic should redraw the map in the correct position.

Related

swiftui mapkit polygon overlay

I'm trying to show a polygon overlay on the map but I don't find what I'm doing wrong
my MapView file is:
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
#EnvironmentObject var vmHome: HomeViewModel
#State var restrictions: [MKOverlay] = []
func makeCoordinator() -> Coordinator {
return MapView.Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
let view = vmHome.mapView
view.showsUserLocation = true
view.delegate = context.coordinator
vmHome.showRestrictedZones { (restrictions) in
self.restrictions = restrictions
print("dentro mapview \(restrictions)")
view.addOverlays(self.restrictions)
}
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
}
class Coordinator: NSObject,MKMapViewDelegate{
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation.isKind(of: MKUserLocation.self){return nil}
else{
let pinAnnotation = MKPinAnnotationView(annotation: annotation, reuseIdentifier: "PIN_VIEW")
pinAnnotation.tintColor = .red
pinAnnotation.animatesDrop = true
pinAnnotation.canShowCallout = true
return pinAnnotation
}
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
renderer.strokeColor = .purple.withAlphaComponent(0.7)
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}
then the view model where I want to convert a fixed array of locations in polygon and add them to MKOverlay array (I cut out some come from the view model that is not related to overlay)
import Foundation
import MapKit
import CoreLocation
class HomeViewModel: NSObject, ObservableObject, CLLocationManagerDelegate{
#Published var mapView = MKMapView()
var overlays: [MKOverlay] = []
func showRestrictedZones(completion: #escaping ([MKOverlay]) -> ()) {
let locations = [CLLocation(latitude: 11.3844028, longitude: 45.6174815), CLLocation(latitude: 11.5608707,longitude: 45.3305094), CLLocation(latitude: 11.8533817, longitude: 45.4447992), CLLocation(latitude: 11.8382755, longitude: 45.6314077), CLLocation(latitude: 11.6624943, longitude: 45.6942722), CLLocation(latitude: 11.3844028, longitude: 45.6174815)]
var coordinates = locations.map({(location: CLLocation) -> CLLocationCoordinate2D in return location.coordinate})
let polygon = MKPolygon(coordinates: &coordinates, count: locations.count)
print(locations.count)
overlays.append(polygon)
print(overlays)
DispatchQueue.main.async {
completion(self.overlays)
}
}
}
ad finally the home view
import SwiftUI
import CoreLocation
struct Home: View {
#EnvironmentObject var vmHome: HomeViewModel
#State var locationManager = CLLocationManager()
var body: some View {
VStack{
HStack{
Text("Hi,")
.font(.title)
.foregroundColor(.theme.primary)
.padding(.horizontal)
Spacer()
VStack(alignment: .trailing) {
HStack {
Image(systemName: "mappin.and.ellipse")
.font(.largeTitle)
.foregroundColor(.blue)
Text("O1")
.font(.title)
.foregroundColor(.theme.primary)
}
Text(vmHome.currentAddress)
.font(.callout)
.foregroundColor(.theme.primary)
}
.padding(.horizontal)
}
ZStack(alignment: .bottom) {
MapView()
.environmentObject(vmHome)
.ignoresSafeArea(.all, edges: .bottom)
//VStack{
Button(action: vmHome.focusLocation, label: {
Image(systemName: "location.fill")
.font(.title2)
.padding(10)
.background(Color.primary)
.clipShape(Circle())
})
.frame(maxWidth: .infinity, alignment: .trailing)
.padding()
.padding(.bottom)
//}
}
}
.background(Color.theme.backgroud)
.onAppear {
locationManager.delegate = vmHome
locationManager.requestWhenInUseAuthorization()
}
.alert(isPresented: $vmHome.permissionDenied, content: {
Alert(title: Text("Permission Denied"), message: Text("Please Enable Permission In App Settings"), dismissButton: .default(Text("Goto Settings"), action: {
// Redireting User To Settings...
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!)
}))
})
}
}
struct Home_Previews: PreviewProvider {
static var previews: some View {
Home()
.environmentObject(HomeViewModel())
}
}
when I debug the array of MKOverlay I have a value like this [<MKPolygon: 0x282200f30>]
so I suppose that inside there's something
thanks
I recommend watching WWDC 2019 Integrating SwiftUI to learn the correct design, from 12:41.
Notice their UIViewRepresentable struct has a #Binding var, which in your case should be the array of polygons or overlays. updateView is called when that value changes and that is where you need to update the MKMapView with the differences in the array from last time. Also you should create the MKMapView in makeUIView do not fetch one from somewhere else.
I would also suggest removing the view model objects and instead learning SwiftUI View structs and property wrappers (which make the efficient structs have view model object semantics). WWDC 2019 Data Flow Through SwiftUI is a great starting point. You'll notice they never use objects for view data.

Change the mapType to .satellite etc with a picker

I want to be able to change the mapType from .standard to .satellite and .hybrid in xCode 13.3 Can anybody tell me if it is at all possible with this code? I implemented a picker to do the job but unfortunately I could not make it work. I succeeded making it change with different code but then buttons map + and map - would not work anymore
import Foundation
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
}
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
Map(coordinateRegion: $region, annotationItems: [quake]) { item in
MapMarker(coordinate: item.coordinate, tint: .red)
} .ignoresSafeArea()
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "plus")
}
}.padding(5)//.border(Color.blue, width: 1)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "minus")
}
}
}.padding(.horizontal)
Text(quake.place)
.font(.headline)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
VStack {
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
.font(.largeTitle)
}
}
}
}
Here is the code that changes the mapType but the buttons do not work anymore:
import Foundation
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
}
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
MapViewUIKit(region: region, mapType: mapType)
.edgesIgnoringSafeArea(.all)
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "plus")
}
}.padding(5)//.border(Color.blue, width: 1)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Text("map")
Image(systemName: "minus")
}
}
}.padding(.horizontal)
Text(quake.place)
.font(.headline)
.bold()
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
//Text("Hybrid flyover").tag(MKMapType.hybridFlyover)
}
.pickerStyle(SegmentedPickerStyle())
.font(.largeTitle)
}
}
}
struct MapViewUIKit: UIViewRepresentable {
let region: MKCoordinateRegion
let mapType : MKMapType
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
I implemented the code and now the buttons work and the mapType changes correctly, thank you very much also for the pointers to the documentation. Unfortunately the annotations do not display the pin at the earthquake location. I changed the title from London to quake.place and the coordinate to coordinate: CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta) but it made no difference. Here are my changes:
import SwiftUI
import MapKit
struct QuakeDetail: View {
var quake: Quake
#State private var region : MKCoordinateRegion
init(quake : Quake) {
self.quake = quake
_region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
}
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
MapViewUIKit(
region: $region,
mapType: mapType,
annotation: Annotation(
title: quake.place,
coordinate: CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta)
) // annotation
).ignoresSafeArea() // MapViewUIKit
Spacer()
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Image(systemName: "plus")
}
}//.padding(5)
Spacer()
QuakeMagnitude(quake: quake)
Spacer()
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Image(systemName: "minus")
}
}
}.padding(.horizontal) // HStack + - buttons and quake magnitude
Text(quake.place)
Text("\(quake.time.formatted())")
.foregroundStyle(Color.secondary)
Text("\(quake.latitude) \(quake.longitude)")
.padding(.bottom, -5)
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
}
}
}
struct Annotation {
let pointAnnotation: MKPointAnnotation
init(title: String, coordinate: CLLocationCoordinate2D) {
pointAnnotation = MKPointAnnotation()
pointAnnotation.title = title
pointAnnotation.coordinate = coordinate
}
}
struct MapViewUIKit: UIViewRepresentable {
#Binding var region: MKCoordinateRegion
let mapType : MKMapType
let annotation: Annotation
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
// Set the delegate so that we can listen for changes and
// act appropriately
mapView.delegate = context.coordinator
// Add the annotation to the map
mapView.addAnnotation(annotation.pointAnnotation)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
// Update your region so that it is now your new region
mapView.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewUIKit
init(_ parent: MapViewUIKit) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// We should handle dequeue of annotation view's properly so we have to write this boiler plate.
// This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
// MKAnnotationView from our annotation.
guard annotation is MKPointAnnotation else { return nil }
let identifier = "Annotation"
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.canShowCallout = true
return annotationView
}
annotationView.annotation = annotation
return annotationView
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// We need to update the region when the user changes it
// otherwise when we zoom the mapview will return to its original region
DispatchQueue.main.async {
self.parent.region = mapView.region
}
}
}
}
So to use MKMapView we need to set up the UIViewRepresentable properly. MKMapView has a delegate and as such we need to set the delegate for our mapView. We do this by adding a Coordinator to our UIViewRepresentable
So here is a full working example, it may not be 100% perfect but it shows the general idea of what you can do.
I created my own ContentView because your code was missing several things (such as Quake).
MapViewUIKit takes three parameters.
A binding for MKCoordinateRegion, it needs to be a binding as we will be passing data back to the ContentView
The mapType which is a MKMapType, this is for changing the map type
An annotation, this is a custom Annotation type that is used to hold the information about the annotation we wish to show on the map.
struct ContentView: View {
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 51.507222,
longitude: -0.1275),
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
)
#State private var mapType: MKMapType = .standard
var body: some View {
VStack {
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
MapViewUIKit(
region: $region,
mapType: mapType,
annotation: Annotation(
title: "London",
coordinate: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275)
)
)
HStack {
Button {
region.span.latitudeDelta *= 0.5
region.span.longitudeDelta *= 0.5
} label: {
HStack {
Image(systemName: "plus")
}
}.padding(5)
Button {
region.span.latitudeDelta /= 0.5
region.span.longitudeDelta /= 0.5
} label: {
HStack {
Image(systemName: "minus")
}
}.padding(5)
}
}
}
}
This is the Annotation struct that I created to hold the information about the annotation that we wish to display.
struct Annotation {
let pointAnnotation: MKPointAnnotation
init(title: String, coordinate: CLLocationCoordinate2D) {
pointAnnotation = MKPointAnnotation()
pointAnnotation.title = title
pointAnnotation.coordinate = coordinate
}
}
Finally we need the UIViewRepresentable to tie it all together. I've commented in the code to show what it does.
struct MapViewUIKit: UIViewRepresentable {
#Binding var region: MKCoordinateRegion
let mapType : MKMapType
let annotation: Annotation
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
// Set the delegate so that we can listen for changes and
// act appropriately
mapView.delegate = context.coordinator
// Add the annotation to the map
mapView.addAnnotation(annotation.pointAnnotation)
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
// Update your region so that it is now your new region
mapView.setRegion(region, animated: true)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewUIKit
init(_ parent: MapViewUIKit) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// We should handle dequeue of annotation view's properly so we have to write this boiler plate.
// This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
// MKAnnotationView from our annotation.
guard annotation is MKPointAnnotation else { return nil }
let identifier = "Annotation"
guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.canShowCallout = true
return annotationView
}
annotationView.annotation = annotation
return annotationView
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// We need to update the region when the user changes it
// otherwise when we zoom the mapview will return to its original region
DispatchQueue.main.async {
self.parent.region = mapView.region
}
}
}
}
This gives the following output
https://imgur.com/a/gH42UED
I would suggest that you familiarise yourself with Apple's documentation and there is a wealth of tutorials out there that can help you.
https://developer.apple.com/documentation/mapkit/mkmapview
https://developer.apple.com/documentation/mapkit/mkmapviewdelegate
https://www.raywenderlich.com/7738344-mapkit-tutorial-getting-started
https://www.hackingwithswift.com/example-code/location/how-to-add-annotations-to-mkmapview-using-mkpointannotation-and-mkpinannotationview
https://talk.objc.io/episodes/S01E195-wrapping-map-view

Passing Map Coordinates to a UIKit Map and Map Pin

I have an app that displays expense entries in a list. Clicking on any entry will display additional information and a map of the transaction location. Each entry contains map coordinates for that particular entry.
I would like to use a UIKit map with the options standard, hybrid, or satellite views. Below is some sample map code that will display the three map types but I need help passing in the coordinates and handling the map pin.
Let me know if you need to see any additional code or have questions about my code. Thanks
struct MapViewUIKit: UIViewRepresentable {
let region: MKCoordinateRegion
let mapType : MKMapType
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: false)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
struct ContentView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 36.15035, longitude: -115.91304) , span: MKCoordinateSpan(latitudeDelta: 0.0005, longitudeDelta: 0.0005))
#State private var mapType: MKMapType = .standard
var body: some View {
ZStack {
MapViewUIKit(region: region, mapType: mapType)
.edgesIgnoringSafeArea(.all)
VStack {
Spacer()
Picker("", selection: $mapType) {
Text("Standard").tag(MKMapType.standard)
Text("Satellite").tag(MKMapType.satellite)
Text("Hybrid").tag(MKMapType.hybrid)
}
.pickerStyle(SegmentedPickerStyle())
.offset(y: -40)
.font(.largeTitle)
}
}
}
}
Below is my current swiftui code with map and map pin showing how the coordinates are passed down to maps. ShowRow is part of logic to display core data entries. Clicking on any entry will bring up additional data and a map.
struct DetailView: View {
var item: CurrTrans // this contains core data entries with coordinates
var coordinate: CLLocationCoordinate2D
var g: GeometryProxy
var body: some View {
VStack {
ShowMap(item: item, coordinate: coordinate)
.frame(width: g.size.width, height: g.size.height * 0.65)
ShowDetail(item: item, g: g) // additional entry info
.padding(.top, g.size.height * 0.05)
}
}
}
struct ShowMap: View {
var coordinate: CLLocationCoordinate2D
var item: CurrTrans
var body: some View {
HStack {
Spacer()
MapView(coordinate: coordinate)
.edgesIgnoringSafeArea(.all)
Spacer()
}
}
}
struct Marker: Identifiable {
let id = UUID()
let location: MapPin // or MapMarker
}
Here in MapView typical UIKit examples hardcode the coordinates in a state parameter. I need to pass in the coordinates dynamically.
struct MapView: View {
var coordinate: CLLocationCoordinate2D
var body: some View {
Map(coordinateRegion: .constant(MKCoordinateRegion(center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005))),
showsUserLocation: false,
annotationItems: [Marker(location: MapPin(coordinate: coordinate))]) { marker in
marker.location
}
}
}
I'm not showing all the details here. The map type segmented picker is shown slightly below the map so I'm not using a ZStack.
The map type state parameter is stored up a level because I have slightly different versions for portrait and landscape modes.
#State private var mapType: MKMapType = .standard
struct ShowMap: View {
var item: CurrTrans
var coordinate: CLLocationCoordinate2D
var g: GeometryProxy
var mapType: MKMapType
var body: some View {
let span = MKCoordinateSpan(latitudeDelta: 0.005, longitudeDelta: 0.005)
let region = MKCoordinateRegion(center: coordinate, span: span)
MapView(region: region, mapType: mapType, coordinate: coordinate)
.edgesIgnoringSafeArea(.all)
}
}
}
struct MapView: UIViewRepresentable {
let region: MKCoordinateRegion
let mapType : MKMapType
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.setRegion(region, animated: true)
// display a map pin
let annotation = MKPointAnnotation()
annotation.coordinate = coordinate
mapView.addAnnotation(annotation)
mapView.mapType = mapType
return mapView
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.mapType = mapType
}
}
struct ShowDetail: View {
var item: CurrTrans
var g: GeometryProxy
#Binding var mapType: MKMapType
var body: some View {
Picker("", selection: $mapType) { // new to end
Text("Default").tag(MKMapType.standard)
Text("Transit").tag(MKMapType.hybrid)
Text("Satellite").tag(MKMapType.satellite)
}
.pickerStyle(SegmentedPickerStyle())
.offset(y: -35)
.font(.largeTitle)
VStack (alignment: .leading) {
ShowMoreDetails(item: item)
.navigationBarTitle("Transaction Details", displayMode: .inline)
.navigationViewStyle(StackNavigationViewStyle())
}
}
}

Problem of resizing Map using SwiftUI Mapkit

I would like to zoom in my map on the application, I tried both using latitudinalMeters: 300, longitudinalMeters: 300 or spin with latitudeDelta: 0.001. Both of them did not work at all.
I also chose (0, 0) as my center, but every time I run on the simulator, I have (37.326010,-122.026056) as my center. Apparently, none of the default settings of center and region that I had set in my location manager works in ContentView.
Here is my code of LocationManager.swift:
import Foundation
import CoreLocation
import MapKit
class LocationManager: NSObject, ObservableObject{
let locationManager = CLLocationManager()
#Published var location: CLLocation?
#Published var region: MKCoordinateRegion
override init(){
self.region = MKCoordinateRegion(center: CLLocationCoordinate2D.init(latitude: 0,longitude: 0),latitudinalMeters: 300, longitudinalMeters: 300)
super.init()
self.locationManager.delegate = self
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest
self.locationManager.requestWhenInUseAuthorization()
self.locationManager.startUpdatingLocation()
}
}
extension LocationManager : CLLocationManagerDelegate {
func locationManager(_ manager: CLLocationManager,
didUpdateLocations locations: [CLLocation]){
guard let location = locations.last else { return }
self.region = MKCoordinateRegion(center: location.coordinate, latitudinalMeters: 300, longitudinalMeters: 300)
self.location = location
}
}
Here is my ContentView:
struct ContentView: View {
var body: some View {
MapView2()
}
}
struct MapView2: View {
#ObservedObject var locationManager = LocationManager()
var body: some View {
let coord = locationManager.location?.coordinate
let lat = coord?.latitude ?? 0
let lon = coord?.longitude ?? 0
return VStack {
Map(coordinateRegion: $locationManager.region,
interactionModes: .all,
showsUserLocation: true, userTrackingMode: .constant(.follow))
}
}
}
As for SwiftUI using MapKit, I would not use the CoreLocation framework. You can use the .onChange modifier to perform zoom changes to your View. You can use the #State var zoom with a SwiftUI gesture to perform them if you want, or anything that can do those changes live. I added two buttons within a slider to zoom in or out for the example.
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
MapsView()
}
}
struct MapsView: View {
#State var zoom: CGFloat = 15
#State var mapCoordinate = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 38.989202809314854,
longitude: -76.93626224283602),
span: MKCoordinateSpan(
latitudeDelta: .zero,
longitudeDelta: .zero))
var body: some View {
VStack(spacing: 16) {
Map(coordinateRegion: $mapCoordinate)
.ignoresSafeArea(edges: .all)
// You can see the changes being operating by the .onChange modifier.
Slider(value: $zoom,
in: 0.01...50,
minimumValueLabel: Image(systemName: "plus.circle"),
maximumValueLabel: Image(systemName: "minus.circle"), label: {})
.padding(.horizontal)
.onChange(of: zoom) { value in
mapCoordinate.span.latitudeDelta = CLLocationDegrees(value)
mapCoordinate.span.longitudeDelta = CLLocationDegrees(value)
}
}
.font(.title)
}
}

How to put a button/view on top of the SwiftUI Map?

I can't find a way to get my buttonView (just a Button) on top of the map so I can tap it.
In another more complicated setup, the button somehow turns-up on top and I can tap it, but this is by luck not by design. How to get my buttonView on the map so I can tap it?
Note, I think the issue maybe that my buttonView is "under" some map layer, hence the map captures the tap events and does not pass them to my buttonView.
Xcode 12 beta-3, mac catalina, target ios 14.
import Foundation
import SwiftUI
import MapKit
import CoreLocation
#main
struct TestMapApp: App {
var body: some Scene {
WindowGroup {
MapViewer()
}
}
}
struct MapViewer: View {
#State var cityAnno = [CityMapLocation(title: "Tokyo", subtitle: "Japan", lat: 35.685, lon: 139.7514)]
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 35.685, longitude: 139.7514),span: MKCoordinateSpan(latitudeDelta: 1.0, longitudeDelta: 1.0))
var body: some View {
Map(coordinateRegion: $region, annotationItems: cityAnno) { city in
MapAnnotation(coordinate: city.coordinate) {
buttonView(cityName: city.title!)
// tried this, does not work
// Image(systemName:"dot.circle.and.cursorarrow").foregroundColor(.white).scaleEffect(2.2)
// .onTapGesture { print("----> onTapGesture") }
}
}
}
func buttonView(cityName: String) -> some View {
Button(action: {print("----> buttonView action")}) {
VStack {
Text(cityName)
Image(systemName: "dot.circle.and.cursorarrow")
}.foregroundColor(.red).scaleEffect(1.2)
}.frame(width: 111, height: 111)
// tried combinations of these, without success
// .background(Color.gray).opacity(0.8)
// .border(Color.white)
// .contentShape(Rectangle())
// .clipShape(Rectangle())
// .zIndex(1)
// .buttonStyle(PlainButtonStyle())
// .layoutPriority(1)
// .allowsHitTesting(true)
// .onTapGesture {
// print("----> onTapGesture")
// }
}
}
class CityMapLocation: NSObject, MKAnnotation, Identifiable {
var id = UUID().uuidString
var title: String?
var subtitle: String?
dynamic var coordinate: CLLocationCoordinate2D
init(title: String?, subtitle: String?, lat: Double, lon: Double) {
self.id = UUID().uuidString
self.title = title
self.subtitle = subtitle
self.coordinate = CLLocationCoordinate2D(latitude: lat, longitude: lon)
}
}
Just wrap them in ZStack:
ZStack {
Map(coordinateRegion: $region, annotationItems: cityAnno){...}
Button(action: {print("----> buttonView action")}) {...}
}
You could get the onTapGesture on Just Vstack. Try bellow code by replacing body of MapViewer.
var body: some View {
Map(coordinateRegion: $region, annotationItems: cityAnno) { city in
MapAnnotation(coordinate: city.coordinate) {
VStack {
Text(city.title ?? "")
Image(systemName: "dot.circle.and.cursorarrow")
}
.foregroundColor(.red).scaleEffect(1.2)
.frame(width: 111, height: 111)
.onTapGesture {
print("Clicked")
}
}
}
}