Polyline not showing in swiftUI - ios

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

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

Map annotations only appear after map is moved

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

How to call a function or tell if a MKPointAnnotation is clicked on a MKMapView SwiftUI

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 { ... }
}

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

Swift: Subclass MKPolyline

I'm developing an app that request multiple directions (MKDirectionsRequest) and draw the routes in a mapView, all ok.
But I'm facing a problem: I want to draw each route with different color.
The first idea was simple: use title/subtitle to 'tag' different MKPolyline so I can set the color I want in the delegate function:
mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer
but I don't like this solution because it's 'ugly' and I will have to parse a string the day I will have to pass different params (traffic ..)
The second simple solution was to subclass MKPolyline, yeh simple ..
as MKPolyline doesn't have a designated initializer, this is impossible (is it ?)
[edit]: I want to create a subclass of MKPolyline to copy "on it" the already created MKPolyline returned by MKDirectionsRequest.routes but I can't figure out how to override the read-only params (Apple says that we should override them in the subclass and add the setter, but I have an infinite loop in the setter witch is .. normal)
if was using objC, it will be simple to "inject" code at runtime and add my params, but I'm using swift.
could anyone help on this, thanks.
Simpler method not requiring a custom renderer :
import UIKit
import MapKit
class CustomPolyline : MKPolyline {
var color: UIColor?
}
class ViewController: UIViewController, MKMapViewDelegate {
#IBOutlet weak var mapView: MKMapView!
override func viewDidLoad() {
super.viewDidLoad()
// setup mapView
mapView.delegate = self
// set map view region
let location : CLLocationCoordinate2D = CLLocationCoordinate2DMake(51.4987, 0.007);
let viewRegion = MKCoordinateRegionMakeWithDistance(location, 400, 400)
mapView.setRegion(viewRegion, animated:true )
// add red line
let coords1 = [CLLocationCoordinate2D(latitude: 51.499526, longitude: 0.004785),CLLocationCoordinate2D(latitude: 51.500007, longitude: 0.005493)]
let polyline1 = CustomPolyline(coordinates: coords1, count: coords1.count)
polyline1.color = UIColor.red
mapView.add(polyline1)
// add blue line
let coords2 = [CLLocationCoordinate2D(latitude: 51.498103, longitude: 0.007574), CLLocationCoordinate2D(latitude: 51.498190, longitude: 0.009677)]
let polyline2 = CustomPolyline(coordinates: coords2, count: coords2.count)
polyline2.color = UIColor.blue
mapView.add(polyline2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
// Dispose of any resources that can be recreated.
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is CustomPolyline {
let polylineRenderer = MKPolylineRenderer(overlay: overlay)
polylineRenderer.strokeColor = (overlay as! CustomPolyline).color
polylineRenderer.lineWidth = 4
return polylineRenderer
}
return MKOverlayRenderer()
}
}
Why do you want to set color to your custom MKOverlay, couldnt you simply set the desired color to MKOverlayRenderer ? It should be quite simpler then.
You could also easily subclass MKPolyline. I dont see why you were not able to do that.
If you want to be able to create a custom MKOverlay and set custom properties, then, draw it into the map, you would also need to create your own renderer. That seems like a big job. But, here is an example how you could do that.
import UIKit
import MapKit
public class CustomOverlay: MKPolyline {
public var customColor: UIColor?
}
public class CustomRenderer: MKPolylineRenderer {
override public func strokePath(_ path: CGPath, in context: CGContext) {
guard let overlayColor = self.overlay as? CustomOverlay, let color = overlayColor.customColor else {
super.strokePath(path, in: context)
return
}
context.saveGState()
context.setStrokeColor(color.cgColor)
context.addPath(path)
context.drawPath(using: .stroke)
context.restoreGState()
}
}
public extension CLLocationCoordinate2D {
static let Salo = CLLocationCoordinate2DMake(60.3909, 23.1355)
static let Turku = CLLocationCoordinate2DMake(60.454510, 22.264824)
static let Helsinki = CLLocationCoordinate2DMake(60.170833, 24.9375)
}
public class ViewController: UIViewController, MKMapViewDelegate {
private var mapView: MKMapView!
override public func viewDidLoad() {
super.viewDidLoad()
createMapView()
setupMapView()
}
private func createMapView() {
mapView = MKMapView(frame: .zero)
mapView.translatesAutoresizingMaskIntoConstraints = false
mapView.delegate = self
view.addSubview(mapView)
[mapView.topAnchor.constraint(equalTo: view.topAnchor),
mapView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
mapView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
mapView.trailingAnchor.constraint(equalTo: view.trailingAnchor)].forEach { $0.isActive = true }
}
private func setupMapView() {
let coordinates: [CLLocationCoordinate2D] = [.Helsinki, .Turku]
let customPolyLine = CustomOverlay(coordinates: coordinates, count: coordinates.count)
customPolyLine.customColor = UIColor.red
mapView.add(customPolyLine)
let coordinateSpan = MKCoordinateSpan(latitudeDelta: 3, longitudeDelta: 3)
let region = MKCoordinateRegion(center: .Salo, span: coordinateSpan)
mapView.setRegion(region, animated: true)
}
// MARK: MKMapViewDelegate
public func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
/* return a simple renderer */
// let renderer = MKPolylineRenderer(overlay:overlay)
// renderer.lineWidth = 2
// renderer.lineDashPattern = [1, 2, 1]
// renderer.strokeColor = UIColor.red
// return renderer
/* a custom renderer */
let customRenderer = CustomRenderer(overlay: overlay)
customRenderer.lineWidth = 2
customRenderer.strokeColor = UIColor.green // this color is not used, since we apply color from overlay inside strokePath(:inContext:) method for custom renderer
customRenderer.lineDashPattern = [1, 2, 1]
return customRenderer
}
}

Resources