Accessing Mkmapview Elements as Uiviewrepresentable in the Main (Contentview) Swiftui View

Accessing MKMapView elements as UIViewRepresentable in the main (ContentView) SwiftUI view

In SwiftUI DSL you don't access views.

Instead, you combine "representations" of them to create views.

A pin can be represented by an object - manipulating the pin will also update the map.

This is our pin object:

class MapPin: NSObject, MKAnnotation {

let coordinate: CLLocationCoordinate2D
let title: String?
let subtitle: String?
let action: (() -> Void)?

init(coordinate: CLLocationCoordinate2D,
title: String? = nil,
subtitle: String? = nil,
action: (() -> Void)? = nil) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
self.action = action
}

}

Here's my Map, which is not just UIViewRepresentable, but also makes use of a Coordinator.

(More about UIViewRepresentable and coordinators can be found in the excellent WWDC 2019 talk - Integrating SwiftUI)

struct Map : UIViewRepresentable {

class Coordinator: NSObject, MKMapViewDelegate {

@Binding var selectedPin: MapPin?

init(selectedPin: Binding<MapPin?>) {
_selectedPin = selectedPin
}

func mapView(_ mapView: MKMapView,
didSelect view: MKAnnotationView) {
guard let pin = view.annotation as? MapPin else {
return
}
pin.action?()
selectedPin = pin
}

func mapView(_ mapView: MKMapView, didDeselect view: MKAnnotationView) {
guard (view.annotation as? MapPin) != nil else {
return
}
selectedPin = nil
}
}

@Binding var pins: [MapPin]
@Binding var selectedPin: MapPin?

func makeCoordinator() -> Coordinator {
return Coordinator(selectedPin: $selectedPin)
}

func makeUIView(context: Context) -> MKMapView {
let view = MKMapView(frame: .zero)
view.delegate = context.coordinator
return view
}

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

uiView.removeAnnotations(uiView.annotations)
uiView.addAnnotations(pins)
if let selectedPin = selectedPin {
uiView.selectAnnotation(selectedPin, animated: false)
}

}

}

The idea is:

  • The pins are a @State on the view containing the map, and are passed down as a binding.
  • Each time a pin is added or removed, it will trigger a UI update - all the pins will be removed, then added again (not very efficient, but that's beyond the scope of this answer)
  • The Coordinator is the map delegate - I can retrieve the touched MapPin from the delegate methods.

To test it:

struct ContentView: View {

@State var pins: [MapPin] = [
MapPin(coordinate: CLLocationCoordinate2D(latitude: 51.509865,
longitude: -0.118092),
title: "London",
subtitle: "Big Smoke",
action: { print("Hey mate!") } )
]
@State var selectedPin: MapPin?

var body: some View {
NavigationView {
VStack {
Map(pins: $pins, selectedPin: $selectedPin)
.frame(width: 300, height: 300)
if selectedPin != nil {
Text(verbatim: "Welcome to \(selectedPin?.title ?? "???")!")
}
}
}

}

}

...and try zooming/tapping the pin on London, UK :)

Sample Image

UIViewRepresentable - MKMapView doesn't get updated

Two issues create map view inside makeView - that's a place for that by design, update annotations right in update (no delay needed) it is a place called on binding update exactly to update UIView counterpart (all UI invalidation/refreshing is made automatically)

So, here is fixed version. Tested with Xcode 13.4 / iOS 15.5

demo

struct MapView: UIViewRepresentable {
@Binding var persons: [Person]

init(persons: Binding<[Person]>) {
self._persons = persons
// other stuff
}

func makeUIView(context: Context) -> MKMapView {

let mapView = MKMapView() // << here !!

mapView.delegate = context.coordinator
mapView.register(MKMarkerAnnotationView.self, forAnnotationViewWithReuseIdentifier: "testAnnotation")
return mapView
}

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

mapView.removeAnnotations(mapView.annotations) // << here !!
mapView.addAnnotations(persons)

}
}

SwiftUI Button on top of a MKMapView does not get triggered

Give it just a bit more internal space to be better recognizable. Here is fixed & tested variant (Xcode 12 / iOS 14):

struct TestButtonWithMap: View {
@State private var locked = true
var body: some View {
ZStack {
MapView()
Button(action: {
print("Tapped")
self.locked.toggle()
}) {
Image(systemName: locked ? "lock.fill" : "lock.open")
.padding() // << here !!
}
}
}
}

SwiftUI: Interaction between Views

Nevermind, i solved it by myself after @vadian's comment (thank you mate).

ContentView:

import SwiftUI
import MapKit

struct ContentView: View
{
@State private var expandedInfoView: Bool = false
@State private var buttonDecision: Int = 0

var body: some View
{
ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom))
{
GeometryReader
{
proxy in
VStack(alignment: .center)
{
MapView(mapCenter: $buttonDecision)
VStack
{
HStack
{
Spacer()
MapViewControls(mapCenter: $buttonDecision)
.padding(.top, -700)
}
}
LogoView()
.onTapGesture
{
withAnimation
{
expandedInfoView.toggle()
}
}
.padding(.top, -150)
}
.sheet(isPresented: $expandedInfoView)
{
InfoView()
}
}
}
}
}

struct ContentView_Previews: PreviewProvider
{
static var previews: some View
{
ContentView()
}
}

MapView:

import SwiftUI
import MapKit
import CoreLocation

struct MapView: UIViewRepresentable
{
@Binding var mapCenter: Int
let fszCoordinates = CLLocation(latitude: XXX, longitude: YYY)
var locationManager = CLLocationManager()

func setupManager()
{
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.requestWhenInUseAuthorization()
locationManager.requestAlwaysAuthorization()
}

func makeUIView(context: Context) -> MKMapView
{
setupManager()

let fszAnnotation = MKPointAnnotation()
fszAnnotation.coordinate = CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude)
fszAnnotation.title = "TITLE"
fszAnnotation.subtitle = "SUBTITLE"

let mapView = MKMapView(frame: UIScreen.main.bounds)
mapView.userTrackingMode = .follow
mapView.showsScale = true
mapView.showsCompass = true
mapView.setCenter(CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude), animated: true)
mapView.showAnnotations([fszAnnotation], animated: false)

let buttonLocateUser = MKUserTrackingButton(mapView: mapView)
buttonLocateUser.layer.backgroundColor = UIColor(white: 1, alpha: 0.8).cgColor
buttonLocateUser.layer.borderColor = UIColor.white.cgColor
buttonLocateUser.layer.borderWidth = 1
buttonLocateUser.layer.cornerRadius = 5
buttonLocateUser.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(buttonLocateUser)

let scale = MKScaleView(mapView: mapView)
scale.legendAlignment = .trailing
scale.translatesAutoresizingMaskIntoConstraints = false
mapView.addSubview(scale)

NSLayoutConstraint.activate([buttonLocateUser.bottomAnchor.constraint(equalTo: mapView.bottomAnchor, constant: -25),
buttonLocateUser.trailingAnchor.constraint(equalTo: mapView.trailingAnchor, constant: -10),
scale.trailingAnchor.constraint(equalTo: buttonLocateUser.leadingAnchor, constant: -10),
scale.centerYAnchor.constraint(equalTo: buttonLocateUser.centerYAnchor)])

return mapView
}

func updateUIView(_ uiView: MKMapView, context: Context)
{
if ($mapCenter.wrappedValue == 0)
{
uiView.setCenter(CLLocationCoordinate2D(latitude: fszCoordinates.coordinate.latitude, longitude: fszCoordinates.coordinate.longitude), animated: true)
}
else if ($mapCenter.wrappedValue == 1)
{
uiView.setCenter(CLLocationCoordinate2D(latitude: uiView.userLocation.coordinate.latitude, longitude: uiView.userLocation.coordinate.longitude), animated: true)
}
}
}

struct MapView_Previews: PreviewProvider
{
@State static var fszCoordinates = 0
static var previews: some View
{
MapView(mapCenter: $fszCoordinates)
}
}

MapViewControls:

import SwiftUI

struct MapViewControls: View
{
@Binding var mapCenter: Int

var body: some View
{
VStack(spacing: 6)
{
VStack(spacing: 12)
{
Button
{
buttonActionZoomToFSZIT()
}
label:
{
Image(systemName: "house.circle")
}
Divider()
Button
{
buttonActionZoomToUser()
}
label:
{
Image(systemName: "location.circle")
}
}
.frame(width: 40)
.padding(.vertical, 12)
.background(Color(UIColor.secondarySystemGroupedBackground))
.cornerRadius(8)
}
.font(.system(size: 20))
.foregroundColor(.blue)
.padding()
.shadow(color: Color(UIColor.black.withAlphaComponent(0.1)), radius: 4)
}

func buttonActionZoomToFSZIT()
{
self.mapCenter = 0
}

func buttonActionZoomToUser()
{
self.mapCenter = 1
}
}

struct MapViewControls_Previews: PreviewProvider
{
@State static var fszCoordinates = 0

static var previews: some View
{
MapViewControls(mapCenter: $fszCoordinates)
}
}

SwiftUI Views blocking touches to MapKit but not other views

Yes, even transparent images do not allow hit-through so far. In your case, simple enough, the possible approach is to create custom shape, like below. Shapes do pass hits.

Here is simple demo of shape to show the direction.

demo

struct Cross: Shape {
func path(in rect: CGRect) -> Path {
return Path { path in
path.move(to: CGPoint(x: rect.midX, y: 0))
path.addLine(to: CGPoint(x: rect.midX, y: rect.maxY))
path.move(to: CGPoint(x: 0, y: rect.midY))
path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
path.move(to: CGPoint(x: rect.midX, y: rect.midY))
path.addArc(center: CGPoint(x: rect.midX, y: rect.midY), radius: 10, startAngle: Angle(degrees: 0), endAngle: Angle(degrees: 360), clockwise: false)
}
}
}

struct ContentView: View {
var body: some View {
ZStack {
MapView()
Cross().stroke(Color.red)
.frame(width: 90, height: 90)
}
}
}


Related Topics



Leave a reply



Submit