Passing Map Coordinates to a UIKit Map and Map Pin - swiftui

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

Related

Can I hide default points of interests in MapKit with SwiftUI like parks, restaurants etc?

I want to use my own map annotations and I am trying to hide default annotations from Map.
I found this to remove every default annotation from the map view
let configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
But how do I apply this configuration to my map view in SwiftUI?
import SwiftUI
import MapKit
#available(iOS 16.0, *)
struct MyMapView: View {
init (){
let configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
}
#State private var region = MKCoordinateRegion(
center: CLLocationCoordinate2D(
latitude: 41.59710,
longitude: -74.14976),
span: MKCoordinateSpan(
latitudeDelta: 0.0125,
longitudeDelta: 0.0125)
)
var body: some View {
Map(coordinateRegion: $region)
.edgesIgnoringSafeArea(.all)
.disabled(true)
}
}
The SwiftUI Map view doesn't support this functionality. To get a map that can do this you'd need to use an MKMapView which is in UIKit. Here's an example of how to use it in SwiftUI
import SwiftUI
import MapKit
struct ContentView: View {
var body: some View {
MapView()
.edgesIgnoringSafeArea(.all)
.disabled(true)
}
}
struct MapView: UIViewRepresentable {
let configuration: MKStandardMapConfiguration
private var center: CLLocationCoordinate2D {
CLLocationCoordinate2D(latitude: 41.59710, longitude: -74.14976)
}
private var coordinateSpan: MKCoordinateSpan {
MKCoordinateSpan(latitudeDelta: 0.0125, longitudeDelta: 0.0125)
}
init() {
configuration = MKStandardMapConfiguration()
configuration.pointOfInterestFilter = MKPointOfInterestFilter(including: [])
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.region = MKCoordinateRegion(center: center, span: coordinateSpan)
mapView.preferredConfiguration = configuration
return mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) { }
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

swiftUI Mapkit and Corelocation causing problems

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.

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

Transferring Coordinates to a MapView with Pin

I am trying to display a map pin on a map. Upon entry of a transaction the details are saved along with the location coordinates. In a list of transaction entries, the user may click on an entry for more detail information including a small map showing the transaction location.
Based on Asperi's suggestions at adding a MapMarker to MapKit in swiftUI 2 it appears that I need to declare an identifiable structure in order to use a map pin.
In the DetailView the latitude and longitude are copied to a coordinate parameter before transmission to MapView.
struct DetailView: View {
var coordinate: CLLocationCoordinate2D {
CLLocationCoordinate2D(
latitude: item.entryLat,
longitude: item.entryLong)
}
var body: some View {
VStack {
MapView(coordinate: coordinate)
.ignoresSafeArea(edges: .all)
.frame(height: 400)
.padding(.vertical, 10)
}
}
}
MapView is where I'm having trouble. I'm not sure how to pass in my coordinates for the region and the marker (xxxxx). Copying` coordinate to the #State region and the marker produces the error "Argument passed to call that takes no arguments".
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
struct MapView: View {
var coordinate: CLLocationCoordinate2D
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(xxxxxxx), span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))
let markers = [Marker(location: MapMarker(coordinate: CLLocationCoordinate2D(xxxxxxx), tint: .red))]
var body: some View {
Map(coordinateRegion: $region, showsUserLocation: true,
annotationItems: markers) { marker in
marker.location
}.edgesIgnoringSafeArea(.all)
}
}
Sounds like for your application, declaring the region as a constant will work. The code would look like this:
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
struct MapView: View {
var coordinate: CLLocationCoordinate2D
var body: some View {
Map(coordinateRegion: .constant(MKCoordinateRegion(center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5))),
showsUserLocation: true,
annotationItems: [Marker(location: MapMarker(coordinate: coordinate))]) { marker in
marker.location
}.edgesIgnoringSafeArea(.all)
}
}
If you still wanted to use it as a #State variable, you could use a custom init to set the value:
struct MapView: View {
var coordinate: CLLocationCoordinate2D
#State private var region : MKCoordinateRegion
init(coordinate : CLLocationCoordinate2D) {
self.coordinate = coordinate
_region = State(initialValue: MKCoordinateRegion(center: coordinate,
span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)))
}
var body: some View {
Map(coordinateRegion: $region,
showsUserLocation: true,
annotationItems: [Marker(location: MapMarker(coordinate: coordinate))]) { marker in
marker.location
}
.edgesIgnoringSafeArea(.all)
}
}
Lastly, I'm defining the markers array inline, but you could split it out into a computed property:
var markers : [Marker] {
[Marker(location: MapMarker(coordinate: coordinate))]
}
var body: some View {
Map(coordinateRegion: $region,
showsUserLocation: true,
annotationItems: markers) { marker in
marker.location
}
.edgesIgnoringSafeArea(.all)
}