Transferring Coordinates to a MapView with Pin - swiftui

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

Related

How to add a marker by click on MapKit view using SwiftUI

I have a fairy simple SwiftUI code block showing a map with a pin. This works so far.
I'd like to add a new pin by a list of draggable objects. Now, I am bit stuck at the following problems:
Where to add a list of draggable items to create the pin?
How to select the item and translate it to a marker object?
How to determine the final location in MapKit where the pin is dragged?
I think it is solvable and I can do it in LeafletJS, but SwiftUI makes my head ache.
import MapKit
import CoreLocation
struct Marker: Identifiable {
let id = UUID()
var location: MapMarker
}
struct ContentView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 47.8681, longitude: 8.205), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.50))
let markers = [Marker(location: MapMarker(coordinate: CLLocationCoordinate2D(latitude: 47.8681, longitude: 8.205), tint: .blue))]
var body: some View {
Map(coordinateRegion: $region, showsUserLocation: true, userTrackingMode: .constant(.follow),annotationItems: markers){ marker in
marker.location
}
.frame(width: 1200, height: 600)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}

How to add tooltip to Map Annotation in order to show the location name on the Map using MapKit (SwiftUI)

I'm trying to figure out how I can display the title/name of a MapAnnotation when I hover over the Annotation/Marker or when I simply tap on the annotation/Marker. Is there a simple way to do this?
I tried using .help(), but it doesn't display anything on the map...
Here is the relevant code...
Map(coordinateRegion: $viewModel.region, showsUserLocation: true, annotationItems: viewModel.locations){ location in
MapAnnotation(coordinate: location.coordinate) {
Image(systemName: "mappin.circle")
.help("\(location.name)")
}
}
Actually, .help() will work as long as you use it with a button.
This is a quick paste from a current project I’ve got.
Note:
This works with the Mac version and I have not tested anywhere else
import SwiftUI
import MapKit
struct SwiftUIMapViewTest: View {
#EnvironmentObject var modelData: ModelData
#State var region: MKCoordinateRegion = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: 29.548460, longitude: -98.481556),
span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
#Binding var selectedListing: Place?
#Binding var results: [Place]
#State var image: String = "mappin"
var body: some View {
Map(
coordinateRegion: $region,
annotationItems: results,
annotationContent: {
listing in
MapAnnotation(coordinate: listing.coordinate,
content: {Button(action: {
self.selectedListing = listing as Place?
},
label: {
VStack{
Image(systemName: "mappin.circle.fill")
.foregroundColor(.red)
.contentShape(Circle())
}
}).help("\(listing.company) \n\(listing.street) \n\(String(listing.zipCode))")
}
)})
}
}
struct SwiftUIMapViewTest_Previews: PreviewProvider {
static var modelData = [ModelData().places]
static var modelPlace = ModelData().places
static var previews: some View {
SwiftUIMapViewTest(
selectedListing: .constant(modelPlace[0]),
results: .constant(modelData[0])
)
}
}
Although not very pritty, this is the result.
Hope this helps someone.
You don't add a tooltip to a map annotation. You make your own custom view. You can display it however you want, and show and hide child views as desired. As an example:
import SwiftUI
import CoreLocation
import MapKit
struct MapAnnotationsView: View {
#State private var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 38.889499, longitude: -77.035230), span: MKCoordinateSpan(latitudeDelta: 0.01, longitudeDelta: 0.01))
let placeArray: [Place] = [Place(title: "Washington Monument", coordinate: CLLocationCoordinate2D(latitude: 38.889499, longitude: -77.035230))]
var body: some View {
Map(coordinateRegion: $region, annotationItems: placeArray) { annotation in
// This makes a generic annotation that takes a View
MapAnnotation(coordinate: annotation.coordinate) {
// This is your custom view
AnnotationView(placeName: annotation.title)
}
}
}
}
struct AnnotationView: View {
let placeName: String
#State private var showPlaceName = false
var body: some View {
VStack(spacing: 0) {
Text(placeName)
.font(.callout)
.padding(5)
.background(Color.white)
.cornerRadius(10)
// Prevents truncation of the Text
.fixedSize(horizontal: true, vertical: false)
// Displays and hides the place name
.opacity(showPlaceName ? 1 : 0)
// You can use whatever you want here. This is a custom annotation marker
// made to look like a standard annotation marker.
Image(systemName: "mappin.circle.fill")
.font(.title)
.foregroundColor(.red)
Image(systemName: "arrowtriangle.down.fill")
.font(.caption)
.foregroundColor(.red)
.offset(x: 0, y: -5)
}
.onTapGesture {
withAnimation(.easeInOut) {
showPlaceName.toggle()
}
}
}
}
struct Place: Identifiable {
let id = UUID()
var title: String
var coordinate: CLLocationCoordinate2D
}

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