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 touchedMapPin
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 :)
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
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.
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
Swift Convert String to Unsafemutablepointer<Int8>
Difference Between Using Objectidentifier() and '===' Operator
Swift - UI Button Shadow Gradient
Change Background Color of Texteditor in Swiftui
Writing Data to an Nsoutputstream in Swift 3
How to Change Font Size and Font Name of Uisegmentedcontrol Programmatically on Swift
Binding an Element of an Array of an Observableobject:'Subscript(_:)' Is Deprecated
Is There an Alternative to Initialize() in MACos Now That Swift Has Deprecated It
iOS 10 Imessage App Extension: How to Calculate the Height of the Extra Tall Navbar
How to Add an Optional String Extension
When to Use [Self] VS [Weak Self] in Swift Blocks
Avaudioengine Crashes When Plug Headphones in or Out
Reference as Key in Swift Dictionary
Nstextfield, Change Text in Swift