I currently have a SwiftUI view that displays a map full of pins. The map has a delegate which can determine when a pin is selected and get the custom object associated with that pin.
The problem is that once the delegate determines that the pin has been selected, I need my SwiftUI view to navigate/push to a detail page with the custom object that the delegate retrieved.
The best comparison I can think of is trying to treat the selected annotation as a navigation link button for the main view.
How do I communicate from the map delegate to the SwiftUI view?
import SwiftUI
import MapKit
/// The SwiftUI view I am referring to
struct ContentView: View {
var body: some View {
ZStack {
MapView()
}
.edgesIgnoringSafeArea(.all)
}
}
/// The custom object I am referring to
class LandmarkAnnotation: NSObject, MKAnnotation {
let title: String?
let subtitle: String?
let coordinate: CLLocationCoordinate2D
init(title: String?,
subtitle: String?,
coordinate: CLLocationCoordinate2D) {
self.title = title
self.subtitle = subtitle
self.coordinate = coordinate
}
}
/// The delegate I am referring to
class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapViewController: MapView
init(_ control: MapView) {
self.mapViewController = control
}
func mapView(_ mapView: MKMapView, viewFor
annotation: MKAnnotation) -> MKAnnotationView?{
...
return annotationView
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let circle = MKCircleRenderer(overlay: overlay)
...
return circle
}
/// This is where the delegate gets the object for the selected annotation
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if let v = view.annotation as? LandmarkAnnotation {
print(v.coordinate)
}
}
}
struct MapView: UIViewRepresentable {
var markers: [CLLocationCoordinate2D] = [CLLocationCoordinate2D(
latitude: 34.055404, longitude: -118.249278),CLLocationCoordinate2D(
latitude: 34.054097, longitude: -118.249664), CLLocationCoordinate2D(latitude: 34.053786, longitude: -118.247636)]
var convertedMarkers: [LandmarkAnnotation] = []
init() {
convertedMarkers = cordToMark(locations: self.markers)
}
func makeUIView(context: Context) -> MKMapView{
MKMapView(frame: .zero)
}
func cordToMark(locations: [CLLocationCoordinate2D]) -> [LandmarkAnnotation] {
var marks: [LandmarkAnnotation] = []
for cord in locations {
let mark = LandmarkAnnotation(title: "Test", subtitle: "Sub", coordinate: cord)
marks.append(mark)
}
return marks
}
func makeCoordinator() -> MapViewCoordinator{
MapViewCoordinator(self)
}
func updateUIView(_ view: MKMapView, context: Context){
let coordinate = CLLocationCoordinate2D(
latitude: 34.0537767, longitude: -118.248)
let mapCamera = MKMapCamera()
mapCamera.centerCoordinate = coordinate
mapCamera.pitch = 10
mapCamera.altitude = 3000
view.camera = mapCamera
view.mapType = .mutedStandard
view.delegate = context.coordinator
view.addAnnotations(self.convertedMarkers)
let radiusCircle = MKCircle(center: CLLocationCoordinate2D(
latitude: 34.0537767, longitude: -118.248), radius: 300 as CLLocationDistance)
view.addOverlay(radiusCircle)
let locationCircle = MKCircle(center: CLLocationCoordinate2D(
latitude: 34.0537767, longitude: -118.248), radius: 3 as CLLocationDistance)
view.addOverlay(locationCircle)
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
.environment(\.colorScheme, .light)
}
}
Here is an image of the map that is displayed in the simulator
Here is an idea (scratch)
add a callback to representable
struct MapView: UIViewRepresentable {
var didSelect: (LandmarkAnnotation) -> () // callback
so in ContentView
ZStack {
MapView() { annotation in
// store/pass annotation somewhere, and
// activate navigation link here, eg. via isActive or selection
}
}
activate callback in delegate
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if let v = view.annotation as? LandmarkAnnotation {
print(v.coordinate)
self.mapViewController.didSelect(v) // << here !!
}
}
Related
I am having a problem with the map within my app. I set the starting location as the center of the users location, but when I go to move the map around, it doesn't allow me to and/or moves automatically back to the center.
I have userTrackingMode set to .follow. I can't really think of anything else that might be causing this to happen, although I am fairly new to Xcode and Swift.
Here is where I think the problem occurs:
import Foundation
import SwiftUI
import MapKit
struct UberMapViewRepresentable: UIViewRepresentable {
let mapView = MKMapView()
let locationManager = LocationManager()
#EnvironmentObject var locationViewModel: LocationSearchViewModel
func makeUIView(context: Context) -> some UIView {
mapView.delegate = context.coordinator
mapView.isRotateEnabled = false
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
return mapView
}
func updateUIView(_ uiView: UIViewType, context: Context) {
if let coordinate = locationViewModel.selectedLocationCoordinate {
context.coordinator.addAndSelectAnnotation(withCoordinate: coordinate)
}
}
func makeCoordinator() -> MapCoordinator {
return MapCoordinator(parent: self)
}
}
extension UberMapViewRepresentable {
class MapCoordinator: NSObject, MKMapViewDelegate {
// MARK: - Properties
let parent: UberMapViewRepresentable
// MARK: - Lifecycle
init(parent: UberMapViewRepresentable) {
self.parent = parent
super.init()
}
// MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, didUpdate userLocation: MKUserLocation) {
let region = MKCoordinateRegion(
center: CLLocationCoordinate2D(latitude: userLocation.coordinate.latitude, longitude: userLocation.coordinate.longitude),
span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
)
parent.mapView.setRegion(region, animated: true)
}
// MARK: - Helpers
func addAndSelectAnnotation(withCoordinate coordinate: CLLocationCoordinate2D) {
parent.mapView.removeAnnotations(parent.mapView.annotations)
let anno = MKPointAnnotation()
anno.coordinate = coordinate
parent.mapView.addAnnotation(anno)
parent.mapView.selectAnnotation(anno, animated: true)
parent.mapView.showAnnotations(parent.mapView.annotations, animated: true)
}
}
}
I have a map which loads annotations from the Google API, when the map initially loads all the annotations they are 'placed' as seen through the print in the console, however they won't show up on the map until I move the map once. Does anyone know if I need to call a method to update the map after placing the annotations?
struct ContentView: View {
var locationSearch = LocationSearch()
#State private var mapView = MapView()
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
#EnvironmentObject var sheetManager: SheetManager
var body: some View {
mapView
.popup(with: SheetManager())
.frame(width: UIScreen.screenWidth, height: UIScreen.screenHeight)
}
}
struct MapView: UIViewRepresentable {
#State var region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: -33.7944, longitude: 151.2649), span: MKCoordinateSpan(latitudeDelta: 0.015, longitudeDelta: 0.015))
func updateUIView(_ uiView: MKMapView, context: Context) {
print("FLF: MapView updated")
uiView.setNeedsDisplay()
}
var locationManager = CLLocationManager()
let mapView = MKMapView(frame: CGRect(x: 0, y: 0, width: UIScreen.screenWidth, height: UIScreen.screenHeight))
func setupManager() {
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()
}
func makeUIView(context: Context) -> MKMapView {
setupManager()
mapView.region = ContentView().region
mapView.showsUserLocation = true
mapView.userTrackingMode = .follow
mapView.delegate = context.coordinator // set the delegate to the coordinator
placeMarkersForRegion(region: region)
return mapView
}
func placeMarkersForRegion(region: MKCoordinateRegion) {
var locationSearch = LocationSearch()
locationSearch.performSearch(region: region) { venues in
print("FLF: Placing \(venues.count) marker(s)")
for marker in venues {
let annotation = MKPointAnnotation()
annotation.coordinate = marker.location
annotation.title = marker.name
mapView.addAnnotation(annotation)
}
}
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self) // pass self to the coordinator so it can call `regionDidChangeAnimated`
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Use the 'coordinate' property to get the current location of the map view
let currentRegion = mapView.region
print("FLF: Map has moved")
self.placeMarkersForRegion(region: currentRegion)
// Do something with the current region (e.g. update a state variable or perform a search)
}
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
var parent: MapView // add a property to hold a reference to the parent view
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
// Call the parent's implementation of this method
parent.mapView(mapView, regionDidChangeAnimated: animated)
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
view.canShowCallout = true
view.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
// Get the tapped annotation
guard let annotation = view.annotation else { return }
// Print the title of the annotation
print(annotation.title ?? "Unknown")
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
print("FLF: Marker tapped")
}
}
The UIViewRepresentable and Coordinator aren't implemented correctly. E.g. makeUIView has to init it, but you are initing it as a property on the struct which is immediately lost. Also MapViewCoordinator(self) is a mistake because self, i.e. the struct, is immediately disgarded after SwiftUI has updated.
Another issue is the #State shouldn't hold a View like how your ContentView has a #State for the MapView.
Here is an example of how to use MKMapView with UIViewRepresentable:
struct MKMapViewRepresentable: UIViewRepresentable {
#Binding var userTrackingMode: MapUserTrackingMode
func makeCoordinator() -> Coordinator {
Coordinator()
}
func makeUIView(context: Context) -> MKMapView {
context.coordinator.mapView
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// MKMapView has a strange design that the delegate is called when setting manually so we need to prevent an infinite loop
context.coordinator.userTrackingModeChanged = nil
uiView.userTrackingMode = userTrackingMode == .follow ? MKUserTrackingMode.follow : MKUserTrackingMode.none
context.coordinator.userTrackingModeChanged = { mode in
userTrackingMode = mode == .follow ? MapUserTrackingMode.follow : MapUserTrackingMode.none
}
}
class Coordinator: NSObject, MKMapViewDelegate {
lazy var mapView: MKMapView = {
let mv = MKMapView()
mv.delegate = self
return mv
}()
var userTrackingModeChanged: ((MKUserTrackingMode) -> Void)?
func mapView(_ mapView: MKMapView, didChange mode: MKUserTrackingMode, animated: Bool) {
userTrackingModeChanged?(mode)
}
}
}
I've been trying to call a function when a pin on my map is clicked. I have about ten pins on my map, so how can I determine which pin is pressed and have all the data that the MKPointAnnotation contains?
How each annotation is added to the map:
let map = MKMapView(frame: .zero)
let annotation = MKPointAnnotation()
annotation.coordinate = donator.coordinates
annotation.title = donator.name
annotation.subtitle = donator.car
map.addAnnotation(annotation)
Thanks!
Assuming that you're wrapping your MKMapView inside a UIViewRepresentable struct, add a coordinator with the MKMapViewDelegate protocol to listen for changes on your map:
//Inside your UIViewRepresentable struct
func makeCoordinator() -> Coordinator {
Coordinator()
}
class Coordinator: NSObject, MKMapViewDelegate {
//Delegate function to listen for annotation selection on your map
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
if let annotation = view.annotation {
//Process your annotation here
}
}
}
There are a couple of tutorials out there on how to include an MKMapView in SwiftUI and use delegation to access the MKMapViewDelegate functions through UIViewRepresentable and coordinators.
Following along my suggestion, your previous code would look like so:
struct MapKitView: UIViewRepresentable {
typealias Context = UIViewRepresentableContext<MapKitView>
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
let annotation = MKPointAnnotation()
annotation.coordinate = donator.coordinates
annotation.title = donator.name
annotation.subtitle = donator.car
map.addAnnotation(annotation)
return map
}
//Coordinator code
func makeCoordinator() -> Coordinator { ... }
}
Hi everyone I've been working for a few days to show a straight line on my map. I use swiftUI and mapkit to render the map. what I want to achieve is a straight line between the two annotations, these are shown on the map.
Dit is de code die ik op dit moment heb. Ik hoop dut jullie mij kunnen helpen want ik kom er niet uit.
import MapKit
struct MapViewWorldDetail: UIViewRepresentable {
var StartCoordinate: CLLocationCoordinate2D
var EndCoordinate: CLLocationCoordinate2D
var name: String
#Binding var region: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 50, longitudeDelta: 50)
let region = MKCoordinateRegion(center: self.region, span: span)
// view.mapType = MKMapType.satelliteFlyover;
view.setRegion(region, animated: true)
let annotation = MKPointAnnotation()
annotation.coordinate = StartCoordinate
annotation.title = name
view.addAnnotation(annotation)
let annotationEnd = MKPointAnnotation()
annotationEnd.coordinate = EndCoordinate
annotationEnd.title = name
view.addAnnotation(annotationEnd)
let aPolyline = MKGeodesicPolyline(coordinates: [StartCoordinate, EndCoordinate], count: 2)
view.addOverlay(aPolyline)
}
}
Note on the straight line you are drawing: The line that you are drawing using MKGeodesicPolyline has this note in the Apple Developer Documentation:
MKGeodesicPolyline - When displayed on a two-dimensional map view, the line segment between any two points may appear curved.
Example of working code
In SwiftUI, you'll need to implement the MKMapViewDelegate in a Coordinator class, which is where the Overlay handling is taken care of:
Add this to func updateUIView
func updateUIView(_ view: MKMapView, context: UIViewRepresentableContext<MapView>) {
// Stuff you already had in updateUIView
// :
// adding this at the end is sufficient
mapView.delegate = context.coordinator
}
Add this to your struct, struct MapViewWorldDetail
// MARK: - Coordinator for using UIKit inside SwiftUI.
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
// MARK: - Managing the Display of Overlays
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
print("mapView(_:rendererFor:)")
if let polyline = overlay as? MKPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: polyline)
polylineRenderer.strokeColor = .red
polylineRenderer.lineWidth = 3
return polylineRenderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
Good references to review:
Sample project that implements the Coordinator class & delegates (but does not do overlays)
https://github.com/roblabs/ios-map-ui/tree/master/MapKit-SwiftUI-for-iOS-macOS
Medium posts by others
https://medium.com/better-programming/exploring-mapkit-on-ios-13-1a7a1439e3b6
https://medium.com/flawless-app-stories/mapkit-in-swiftui-c0cc2b07c28a
In my application I'm using MKMapView SwiftUI implementation. My map is working good but I want to get directions when tapped a button from ContentView. I've explained in more detail below...
Here is my ContentView:
struct ContentView: View {
var body: some View {
VStack {
AppleMapView(coordinate: CLLocationCoordinate2D(latitude: 40.7127, longitude: -74.0059))
.frame(height: 400)
Button(action: {
**// !!! I want to call AppleMapView/getDirections() method here !!!**
}) {
Text("Get Directions")
}
}
}
}
Here is my MapView:
struct AppleMapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
// some codes //
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// some codes //
}
func makeCoordinator() -> Coordinator {
return Coordinator()
}
func getDirections() {
// some codes //
}
class Coordinator: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = .blue
renderer.lineWidth = 4
return renderer
}
}
}
Thanks.
Create a #Binding property on your MapView and then set the directions from your ContentView inside the Button action.
This way your updateUIView will get called accordingly with the updated values.
struct ContentView: View {
#State var directions: [CLLocation] = []
var body: some View {
VStack {
MapView(directions: $directions)
Button(action: {
// Directions are Nepal to India
self.directions = [CLLocation(latitude: 27.2041206, longitude: 84.6093928), CLLocation(latitude: 20.7712763, longitude: 73.7317739)]
}) {
Text("Get Directions")
}
}
}
}
struct MapView: UIViewRepresentable {
#Binding var directions: [CLLocation]
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.strokeColor = .blue
renderer.lineWidth = 4
return renderer
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
return MKMapView()
}
func updateUIView(_ mapView: MKMapView, context: Context) {
var coordinates = self.directions.map({(location: CLLocation!) -> CLLocationCoordinate2D in return location.coordinate})
let polyline = MKPolyline(coordinates: &coordinates, count: self.directions.count)
mapView.delegate = context.coordinator
mapView.addOverlay(polyline)
}
}