Swiftui Map Overlays Without UIviewrepresentable

Swiftui how to use MKOverlayRenderer?

You need to specify a delegate if you want mapView(_:rendererFor:) to be called:

struct MapView: UIViewRepresentable {
@Binding var route: MKPolyline?
let mapViewDelegate = MapViewDelegate()

func makeUIView(context: Context) -> MKMapView {
MKMapView(frame: .zero)
}

func updateUIView(_ view: MKMapView, context: Context) {
view.delegate = mapViewDelegate // (1) This should be set in makeUIView, but it is getting reset to `nil`
view.translatesAutoresizingMaskIntoConstraints = false // (2) In the absence of this, we get constraints error on rotation; and again, it seems one should do this in makeUIView, but has to be here
addRoute(to: view)
}
}

private extension MapView {
func addRoute(to view: MKMapView) {
if !view.overlays.isEmpty {
view.removeOverlays(view.overlays)
}

guard let route = route else { return }
let mapRect = route.boundingMapRect
view.setVisibleMapRect(mapRect, edgePadding: UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10), animated: true)
view.addOverlay(route)
}
}

class MapViewDelegate: NSObject, MKMapViewDelegate {
func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
let renderer = MKPolylineRenderer(overlay: overlay)
renderer.fillColor = UIColor.red.withAlphaComponent(0.5)
renderer.strokeColor = UIColor.red.withAlphaComponent(0.8)
return renderer
}
}

Used like so:

struct ContentView : View {
@State var route: MKPolyline?

var body: some View {
MapView(route: $route)
.onAppear {
self.findCoffee()
}
}
}

private extension ContentView {
func findCoffee() {
let start = CLLocationCoordinate2D(latitude: 37.332693, longitude: -122.03071)
let region = MKCoordinateRegion(center: start, latitudinalMeters: 2000, longitudinalMeters: 2000)

let request = MKLocalSearch.Request()
request.naturalLanguageQuery = "coffee"
request.region = region

MKLocalSearch(request: request).start { response, error in
guard let destination = response?.mapItems.first else { return }

let request = MKDirections.Request()
request.source = MKMapItem(placemark: MKPlacemark(coordinate: start))
request.destination = destination
MKDirections(request: request).calculate { directionsResponse, _ in
self.route = directionsResponse?.routes.first?.polyline
}
}
}
}

Yielding:

Coffee

Display overlay view when selecting annotation in SwiftUI

You want to know the selected annotation in your SwiftUI view. So you have to store it somewhere. Declare a @State :

struct ContentView: View {
let locations: [MKAnnotation]
@State private var selectedLocation: MKAnnotation?
var body: some View {
// ... //
}
}

Now in your wrapper (UIViewRepresentable) you have to make a binding with this MKAnnotation? :

struct MapView: UIViewRepresentable {
@Binding var selectedLocation: MKAnnotation? // HERE
let annotations: [MKAnnotation]

func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
mapView.region = // .... //
mapView.addAnnotations(annotations)
mapView.delegate = context.coordinator
return mapView
}

func updateUIView(_ view: MKMapView, context: Context) {
// .... //
}

Now you should be able to access this variable in your Delegate (Coordinator). For that you have to pass the UIViewRepresentable to the Coordinator:

    func makeCoordinator() -> Coordinator {
Coordinator(self)
}

class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView

init(_ parent: MapView) {
self.parent = parent
}

And finally in func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) you can copy the MKAnnotation in parent.selectedLocation .
With the @Binding this MKAnnotation is now accessible in your parent view (ContentView). You can display its properties in your DetailView.

        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
// ... //
}

func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
parent.selectedLocation = view.annotation
}
}
}

For example :

struct ContentView: View {
let locations: [MKAnnotation]
@State private var selectedLocation: MKAnnotation?
var body: some View {
VStack {
Text("\(selectedLocation?.coordinate.latitude ?? 0)")

// Don't forget the Binding : $selectedLocation

MapView(selectedLocation: $selectedLocation, annotations: locations)
}
}
}

iOS MapKit - Which overlay was tapped

Ok, I found a solution if anyone else will ever need it.

  1. I added a UITapGestureRecognizer to my map view instance.
  2. I used the MKMapView's convert(_:toCoordinateFrom:) to convert the touch point to the map coordinates
  3. I created a MKMapPoint from that coordinate and checked if the MKPolygon renderer path contains the point
  4. For the MKPolygon, being a MKShape after all, I used the .title property to assign my zone_id value parsed from the GeoJSON.

So I was able to identify which polygon was tapped.

import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {

var tapCoordinates: Binding<CLLocationCoordinate2D>
var polygonID: Binding<String>
@EnvironmentObject var mapData: MapViewModel
@State var restrictions: [MKOverlay] = []
@State var restrictionsData: [RestrictionInfo] = []

func makeCoordinator() -> Coordinator {
return MapView.Coordinator(self, tapCoordinatesBinding: tapCoordinates, polygonTitle: polygonID)
}

func makeUIView(context: Context) -> MKMapView {

mapData.mapView.delegate = context.coordinator

let view = mapData.mapView


view.showsUserLocation = true
view.delegate = context.coordinator

mapData.showRestrictedZones { (restriction) in
self.restrictions = restriction
view.addOverlays(self.restrictions)
}
return view
}

func updateUIView(_ uiView: MKMapView, context: Context) {

}

class Coordinator: NSObject, MKMapViewDelegate, UIGestureRecognizerDelegate {
var parent: MapView
var polygonTitle: Binding<String>
var gRecognizer = UITapGestureRecognizer()

var tapCoordinatesBinding: Binding<CLLocationCoordinate2D>
var coordinate = CLLocationCoordinate2D()


init(_ parent: MapView, tapCoordinatesBinding: Binding<CLLocationCoordinate2D>, polygonTitle: Binding<String>) {
self.parent = parent
self.tapCoordinatesBinding = tapCoordinatesBinding
self.polygonTitle = polygonTitle
super.init()
self.gRecognizer = UITapGestureRecognizer(target: self, action: #selector(tapHandler))
self.gRecognizer.delegate = self
self.parent.mapData.mapView.addGestureRecognizer(gRecognizer)
}


@objc func tapHandler(_ gesture: UITapGestureRecognizer) {
// position on the screen, CGPoint
let location = gRecognizer.location(in: self.parent.mapData.mapView)
// position on the map, CLLocationCoordinate2D
coordinate = self.parent.mapData.mapView.convert(location, toCoordinateFrom: self.parent.mapData.mapView)
tapCoordinatesBinding.wrappedValue = coordinate

for overlay: MKOverlay in self.parent.mapData.mapView.overlays {
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
let mapPoint = MKMapPoint(coordinate)
let rendererPoint = renderer.point(for: mapPoint)
if renderer.path.contains(rendererPoint) {
print("Tap inside polygon")
print("Polygon \(polygon.title ?? "no value") has been tapped")
polygonTitle.wrappedValue = polygon.title!
}
}
}
}


func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
if let polygon = overlay as? MKPolygon {
let renderer = MKPolygonRenderer(polygon: polygon)
renderer.fillColor = UIColor.purple.withAlphaComponent(0.2)
renderer.strokeColor = .purple.withAlphaComponent(0.7)
renderer.lineWidth = 2

return renderer
}
return MKOverlayRenderer(overlay: overlay)
}
}
}
import SwiftUI
import MapKit

// All Map Data Goes here...

class MapViewModel: NSObject, ObservableObject {

@Published var mapView = MKMapView()



func restrictionsInfo(completion: @escaping ([RestrictionInfo]) -> ()) {
guard let url = URL(string: "https://flightplan.romatsa.ro/init/static/zone_restrictionate_uav.json") else {
fatalError("Unable to get geoJSON") }

downloadData(fromURL: url) { (returnedData) in
if let data = returnedData {
var geoJson = [MKGeoJSONObject]()
do {
geoJson = try MKGeoJSONDecoder().decode(data)
} catch {
fatalError("Unable to decode GeoJSON")
}


var restrictionsInfo = [RestrictionInfo]()
for item in geoJson {
if let feature = item as? MKGeoJSONFeature {
let propData = feature.properties!
for geo in feature.geometry {
if geo is MKPolygon {
let polygonInfo = try? JSONDecoder.init().decode(RestrictionInfo.self, from: propData)
restrictionsInfo.append(polygonInfo!)
}
}
}
}
DispatchQueue.main.async {
completion(restrictionsInfo)
}
}
}
}


// Decode GeoJSON from the server
func showRestrictedZones(completion: @escaping ([MKOverlay]) -> ()) {
guard let url = URL(string: "https://flightplan.romatsa.ro/init/static/zone_restrictionate_uav.json") else {
fatalError("Unable to get geoJSON") }

downloadData(fromURL: url) { (returnedData) in
if let data = returnedData {
var geoJson = [MKGeoJSONObject]()
do {
geoJson = try MKGeoJSONDecoder().decode(data)
} catch {
fatalError("Unable to decode GeoJSON")
}

var overlays = [MKOverlay]()
for item in geoJson {
if let feature = item as? MKGeoJSONFeature {
let propData = feature.properties!
for geo in feature.geometry {
if let polygon = geo as? MKPolygon {
let polygonInfo = try? JSONDecoder.init().decode(RestrictionInfo.self, from: propData)
polygon.title = polygonInfo?.zone_id
overlays.append(polygon)
}
}
}
}
DispatchQueue.main.async {
completion(overlays)
}
}
}
}

func downloadData( fromURL url: URL, completion: @escaping (_ data: Data?) -> ()) {
URLSession.shared.dataTask(with: url) { (data, response, error) in
guard
let data = data,
error == nil,
let response = response as? HTTPURLResponse,
response.statusCode >= 200 && response.statusCode < 300 else {
print("Error downloading data.")
completion(nil)
return
}
completion(data)
}
.resume()
}
}


Related Topics



Leave a reply



Submit