UIViewRepresentable wrapped UIKit Map jumps back on moving - swift

I'm trying to draw routes on a map and I had to come to the conclusion that this is not yet possible with SwiftUI. Many tutorials suggested wrapping the old MKMapVIew into a UIViewRepresentable which seems to work fine but I have one problem.
Because I need to be able to change the region from outside, I've added this line into the updateUIView method but this causes issues when I'm moving the map. I think what happens is when I scroll the map it updates the state but it does not do it for all the frames of the movements so when this updated region 'comes back' through the updateUIView method, my map is already in an other position so it jumps back.
How can I only update the region when it is intended?
Here is my code:
struct CustomMap: UIViewRepresentable{
typealias UIViewType = MKMapView
#Binding var region: MKCoordinateRegion
var onMapTaped: ((_ item: MKMapItem) -> Void)?
private let mapview: MKMapView = MKMapView()
func makeUIView(context: Context) -> MKMapView {
mapview.delegate = context.coordinator
mapview.setRegion(region, animated: true)
mapview.showsUserLocation = true
mapview.addGestureRecognizer(context.coordinator.tapRecognizer)
return mapview
}
func updateUIView(_ uiView: MKMapView, context: Context) {
uiView.setRegion(region, animated: false)
}
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator(customMapView: self)
}
class MapViewCoordinator: NSObject, MKMapViewDelegate{
let customMapView: CustomMap
var tapRecognizer: UITapGestureRecognizer = UITapGestureRecognizer()
init(customMapView: CustomMap){
self.customMapView = customMapView
super.init()
self.tapRecognizer = UITapGestureRecognizer(target:self, action: #selector(tapOnMap))
}
func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
DispatchQueue.main.async {
self.customMapView.region = mapView.region
}
}
}
}

No the most elegant solution in my opinion but it seems like there is no better one at the time. I got the inspiration from #Asperi's comment.
I passed a closure to the initializer of my view that accepts an MKMapView . I save that map view in my state so I can update the region on a button tap or whenever I want.

Related

Error when adding polygon overlay to MKMapView

I have a map view that is conforming to UIViewRepresentable to use it in my SwiftUI app. When my view model updates with data after a successful API call, I add to my array of overlays with an array of coordinates to represent a map boundary. This array of overlays is then passed to the view and then the overlay is added as an MKPolygon in updateUIView. I return a render this polygon in the delegate method.
struct MapViewUIKit: UIViewRepresentable {
#Binding var annotations: [MKPointAnnotation]
#Binding var mapRect: MKMapRect
#Binding var overlays: [MKOverlay]
let mapView = MKMapView()
func makeUIView(context: Context) -> MKMapView {
mapView.delegate = context.coordinator
return mapView
}
func updateUIView(_ view: MKMapView, context: Context) {
view.setVisibleMapRect(mapRect, animated: true)
if annotations.count != view.annotations.count {
view.removeAnnotations(view.annotations)
view.addAnnotations(annotations)
}
if overlays.count != view.overlays.count {
view.removeOverlays(view.overlays)
view.addOverlays(overlays)
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapViewUIKit
init(_ parent: MapViewUIKit) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
parent.mapRect = mapView.visibleMapRect
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if overlay is MKPolygon {
let renderer = MKPolygonRenderer(overlay: overlay)
renderer.strokeColor = .magenta
renderer.fillColor = .blue
return renderer
}
return MKOverlayRenderer()
}
}
}
However, when I zoom into the area where the polygon should be rendered, I get the following error and there is no overlay
Wrapped around the polygon without finishing... :-(
List has 6 nodes:
1 2 4 5 6 0
2022-03-14 15:01:36.159966-0400 Synop[3702:59086] [VKDefault] Building failed to triangulate!
I've tried searching what this error means to no avail. I know my overlay is being added and with the proper coordinates as well with print statements in the relevant places.

UIKit update of SwiftUI #State causing "Modifying state during view update"

I have a SwiftUI component loading a MapKit like this :
struct AddressView: View {
#State private var showingPlaceDetails = false
var body: some View {
MapView(showPlaceDetails: self.$showPlaceDetails)
}
}
The MapView component is a MapKit struct using UIKit -> SwiftUI wrapping technique :
struct MapView: UIViewRepresentable {
#Binding var showingPlaceDetails: Bool
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
return map
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
// This is where the warning happen
self.control.showingPlaceDetails = true
}
}
}
So mutating showPlaceDetails is firing this warning : [SwiftUI] Modifying state during view update, this will cause undefined behavior.
How should i clean this code ? Is this implementation correct ?
I understand that i am changing thru a #Binding a parent #State property that will re-render AddressView with MapView.
I understand why its bad, like to change a state property inside a render in React and that this mutation should occur outside the body but how can it be done as i need MapView inside the body ?
XCode Version 11.3.1 (11C504)
macOS Version 10.15.4 Beta (19E224g)
The usual fix for this is pending modification, like below
func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
DispatchQueue.main.async {
self.control.showingPlaceDetails = true
}
}

Google Maps API didDrag, didBeginDragging, didEndDragging functions do not work

I have set up a single marker on the map view and I am using GMSMapViewDelegate and implementing the following functions:
mapView:didBeginDraggingMarker:
mapView:didEndDraggingMarker:
mapView:didDragMarker:
I have tried reinstalling the pods but nothing I do seems to work. I have been stuck on this problem for the past 5 days and still no luck.
import GoogleMaps
class MapViewController: UIViewController, GMSMapViewDelegate {
let marker = GMSMarker()
var mapView = GMSMapView()
override func viewDidLoad() {
super.viewDidLoad()
// This function fetches data from the server and places marker
makeRequest()
self.marker.isDraggable = true
// Create a map with the location based on user's country
let camera = GMSCameraPosition.camera(withLatitude: defaults.value(forKey: map.lat) as! CLLocationDegrees,
longitude: defaults.value(forKey: map.lon) as! CLLocationDegrees,
zoom: defaults.value(forKey: map.zoom) as! Float)
mapView = GMSMapView.map(withFrame: view.bounds, camera: camera)
}
func mapView(_ mapView: GMSMapView, didBeginDragging marker: GMSMarker) {
print("DidBeginDragging")
}
func mapView(_ mapView: GMSMapView, didEndDragging marker: GMSMarker) {
print("didEndDragging")
}
func mapView(_ mapView: GMSMapView, didDrag marker: GMSMarker) {
print("didDrag")
}
}
I can drag the marker but I cannot seem to run the three delegate functions.
Looks like you missed setting the delegate:
mapView.delegate = self
at the top change the var mapView = GMSMapView() to var mapView: GMSMapView?
The way you have it, you're creating a mapView and then creating a new one in viewDidLoad
Documentation

(SwiftUI) MKOverlays (parsed from GeoJSON) are added, but do not show on screen, am I missing something?

I am attempting to show a UIKit/MapKit view in SwiftUI, following code very similar to this project on Github but implementing the GeoJSON render/overlay code from the Apple Guide for Optimizing Map Views with Filtering and Camera Constraints.
I have an 'EventDataSource' class which (successfully) parses the GeoJSON objects from a local file, in a UIViewRepresentable class, I then load this data, and add the overlays in the updateUIView function.
Placing a breakpoint in the mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) function shows that each polygon has the correct value for _pointCount, and I have added a print() line for the coordinates, which successfully prints out the coordinates of all the objects whenever the view is loaded.
Xcode shows no errors and builds fine, when run, the MapKit view loads, yet no overlays appear on screen.
The exact same mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) function, used in a UIKit ViewController renders the objects perfectly. In SwiftUI, I get successful parsing, yet only an empty map. Is there something I am missing?
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var dataSource: EventDataSource = EventDataSource()
func makeUIView(context: Context) -> MKMapView {
let map = MKMapView()
map.delegate = context.coordinator
return map
}
func updateUIView(_ mapView: MKMapView, context: Context) {
mapView.region = .event
mapView.addOverlays(dataSource.overlays)
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
final class Coordinator: NSObject, MKMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor(named: "OverlayFill")
renderer.strokeColor = UIColor(named: "OverlayStroke")
renderer.lineWidth = 1.0
print("\(polygon.coordinate.longitude), \(polygon.coordinate.latitude)") // succesfully prints the coordinate of each object
return renderer
}
else if let multiPolygon = overlay as? MKMultiPolygon {
let renderer = MKMultiPolygonRenderer(multiPolygon: multiPolygon)
renderer.fillColor = UIColor(named: "OverlayFill")
renderer.strokeColor = UIColor(named: "OverlayStroke")
renderer.lineWidth = 1.0
print("\(multiPolygon.coordinate.longitude), \(multiPolygon.coordinate.latitude)") // succesfully prints the coordinate of each object
return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}
I have tried commenting out the line
mapView.addOverlays(dataSource.overlays)
When this line is not there, the coordinates do not print to the console, so I am sure that the function is being called.
Any help will be greatly appreciated, thanks!
if you try this
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
var coordinate: CLLocationCoordinate2D
func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}
func updateUIView(_ view: MKMapView, context: Context) {
let span = MKCoordinateSpan(latitudeDelta: 2.0, longitudeDelta: 2.0)
let region = MKCoordinateRegion(center: coordinate, span: span)
view.setRegion(region, animated: true)
}
}
then in your view add
MapView(coordinate: "the coordinates")
check out the
https://developer.apple.com/tutorials/swiftui/creating-and-combining-views

MKPinAnnotationView doesn't show in MKMapView even though it's properly added using Swift

I'm developing an OSX app.
I've subclassed a MKAnnotation:
import Foundation
import MapKit
class LocationAnnotation: NSObject, MKAnnotation {
var image: NSImage?
var title: String?
var coordinate: CLLocationCoordinate2D
init(image anImage: NSImage?, title aTitle: String, coordinate aCoordinate: CLLocationCoordinate2D) {
self.image = anImage
self.title = aTitle
self.coordinate = aCoordinate
}
}
In my NSViewController subclass, I've added a MKMapViewDelegate and in Interface Builder I've added a MKMapView and set its delegate to NSViewController.
To find out what's wrong, I'm adding three locations in my ViewDidLoad method, into an array. I'm adding those annotations to my map in ViewDidAppear. I plan to move that to a background thread when I figure out what's wrong.
import Cocoa
import MapKit
class ShowLocationsViewController: NSViewController, MKMapViewDelegate {
#IBOutlet var locationMap: MKMapView!
private var myLocationArray: [LocationAnnotation] = []
private var myRegion: MKCoordinateRegion!
//#MARK: - UIViewController
override func viewDidLoad() {
super.viewDidLoad()
myLocationArray = []
myRegion = nil
let locLondon = LocationAnnotation(image: nil, title: "London", coordinate: CLLocationCoordinate2DMake(51.522617, -0.139371))
let locWembley = LocationAnnotation(image: nil, title: "Wembley", coordinate: CLLocationCoordinate2DMake(51.555909, -0.279600))
let locGreenwich = LocationAnnotation(image: nil, title: "Greenwich", coordinate: CLLocationCoordinate2DMake(51.476572, -0.001596))
myLocationArray.append(locLondon)
myLocationArray.append(locWembley)
myLocationArray.append(locGreenwich)
myRegion = MKCoordinateRegion.init(center: locLondon.coordinate, span: MKCoordinateSpanMake(0.20, 0.20)) // 20km span
}
override func viewDidAppear() {
locationMap.addAnnotations(myLocationArray)
locationMap.setRegion(myRegion, animated: true)
}
}
When using delegate's method viewFor annotation: I print out the title of each annotation in a log and all three of them are listed. In another delegate's method didAdd I'm printing out the number of annotations of my map in a log and it prints out three. But on my map, there are no annotations displayed. I tried panning and zooming with no success. Region gets properly set and displayed though.
//#MARK: - MKMapViewDelegate
func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
print(locationMap.annotations.count, views.count)
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
if annotation is LocationAnnotation {
let annotationIdentifier = "Location"
var annotationView = locationMap.dequeueReusableAnnotationView(withIdentifier: annotationIdentifier) as! MKPinAnnotationView?
if annotationView == nil {
annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: annotationIdentifier)
}
annotationView!.canShowCallout = true
print(annotationView!.annotation!.title ?? "no view")
return annotationView
} else {
print("no annotation")
return nil
}
}
When I try the same code in an iOS app, everything works. I assume there is nothing wrong with my delegate because methods are properly called.
I would appreciate any help you can give me.
I figured out the problem, the way the window is being called.
I created a variable mainWindowController in my AppDelegate, then I subclassed my main NSViewController so that I could hook up AppDelegate to it.
let appDelegate = NSApp.delegate as! AppDelegate
appDelegate.mainWindowController = self
Afterwards I used this code to call my window which was errornous:
let mapViewController = mainWindowController?.storyboard?.instantiateController(withIdentifier: NSStoryboard.SceneIdentifier.init(rawValue: "ShowLocationsID")) as! ShowLocationsViewController
mainWindowController?.contentViewController?.presentViewControllerAsModalWindow(mapViewController)
I changed that in a way that I created a segue in my storyboard and called the following code which worked and my pins did show:
performSegue(withIdentifier: NSStoryboardSegue.Identifier(rawValue: "ShowLocations"), sender: self)