Map annotations only appear after map is moved - ios

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

Related

Why is the map automatically zooming in after I manually moved it?

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

SwiftUI / MapKit Annotation Selection and Navigation

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

Detect longpress in MapView for SwiftUI

I have a MapView in SwiftUi and I am trying to add a pin annotation to it when a user long presses a location on the map. I see this can easily be accomplished in swift however I am using SwiftUI. I do not know how to add the long-press detector. A code example would be great.
My MapView
struct MapView: UIViewRepresentable {
#Binding
var annotations: [PinAnnotation]
let addAnnotationListener: (PinAnnotation) -> Void
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
view.delegate = context.coordinator
view.addAnnotations(annotations)
if annotations.count == 1 {
let coords = annotations.first!.coordinate
let region = MKCoordinateRegion(center: coords, span: MKCoordinateSpan(latitudeDelta: 0.1, longitudeDelta: 0.1))
view.setRegion(region, animated: true)
}
}
func makeCoordinator() -> MapViewCoordinator {
MapViewCoordinator(self)
}
}
MapViewCoordinator
class MapViewCoordinator: NSObject, MKMapViewDelegate {
var mapViewController: MapView
init(_ control: MapView) {
self.mapViewController = control
}
func mapView(_ mapView: MKMapView, annotationView view: MKAnnotationView, calloutAccessoryControlTapped control: UIControl) {
let annotation = view.annotation
guard let placemark = annotation as? MKPointAnnotation else { return }
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView?{
//Custom View for Annotation
let identifier = "Placemark"
if let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) {
annotationView.annotation = annotation
return annotationView
} else {
let annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)
annotationView.isEnabled = true
annotationView.canShowCallout = true
let button = UIButton(type: .infoDark)
annotationView.rightCalloutAccessoryView = button
return annotationView
}
}
}
The method to add a pin to a MapView
func addPinBasedOnGesture(gestureRecognizer:UIGestureRecognizer){
var touchPoint = gestureRecognizer.locationInView(mapView)
var newCoordinates = self.convertPoint(touchPoint, toCoordinateFromView: mapView)
let annotation = MKPointAnnotation()
annotation.coordinate = newCoordinates
mapView.addAnnotation(annotation)
}
Below solution adds a pin at the point where user long presses on the Map.
Add below method in MapViewCoordinator
#objc func addPinBasedOnGesture(_ gestureRecognizer:UIGestureRecognizer) {
let touchPoint = gestureRecognizer.location(in: gestureRecognizer.view)
let newCoordinates = (gestureRecognizer.view as? MKMapView)?.convert(touchPoint, toCoordinateFrom: gestureRecognizer.view)
let annotation = PinAnnotation()
guard let _newCoordinates = newCoordinates else { return }
annotation.coordinate = _newCoordinates
mapViewController.annotations.append(annotation)
}
and longPress gesture code in func makeUIView(context: Context) -> MKMapView {}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let longPressed = UILongPressGestureRecognizer(target: context.coordinator,
action: #selector(context.coordinator.addPinBasedOnGesture(_:)))
mapView.addGestureRecognizer(longPressed)
return mapView
}
Find below modified parts of provided code to get required behaviour:
struct SUMapView: UIViewRepresentable {
// ... other your code here
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.delegate = context.coordinator
let longPressed = UILongPressGestureRecognizer(target:
context.coordinator, action: #selector(addPin(gesture:)))
mapView.addGestureRecognizer(longPressed)
return mapView
}
// ... other your code here
}
class MapViewCoordinator: NSObject, MKMapViewDelegate {
// ... other your code here
#objc func addPin(gesture: UILongPressGestureRecognizer) {
// do whatever needed here
}
// ... other your code here
}

Polyline not showing in swiftUI

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

How to get MKMapView Directions in SwiftUI

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

Resources