swiftuimapkitmkmapview

Change maptype based on button selection using MKMapView


So I'm trying to click on a button and change the mapType using the MKMapView API, but I can't seem to achieve it.

So here is what I have, we have the MKMapView file:

import SwiftUI
import MapKit

struct MapViewUIKit: UIViewRepresentable {

    // Environment Objects
    @EnvironmentObject var mainViewModel: MainViewModel
    @EnvironmentObject private var mapSettings: MapSettings
    
    // Coordinator function
    final class Coordinator: NSObject, MKMapViewDelegate {
        
        // Define this class.
        var parent: MapViewUIKit
    
        // Initialize this class.
        init(_ parent: MapViewUIKit) {
            self.parent = parent
        }
        
        // MARK: Display Annotation
        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            // Unrelated code here.
        }
        
        // MARK: Select Annotation
        func mapView(_ mapView: MKMapView, didSelect view: MKAnnotationView) {
             // Unrelated code here.
        }
    }
    
    //
    func makeCoordinator() -> Coordinator {
        MapViewUIKit.Coordinator(self)
    }
    
    // MARK: CREATE MAP
    func makeUIView(context: Context) -> MKMapView {
        
        // Create map.
        let mapView = MKMapView(frame: .zero)
        
        // Coordinate our delegate.
        mapView.delegate = context.coordinator
        
        // Set our region for the map.
        mapView.setRegion(DEFAULT_MK_REGION, animated: false)
        
        // Set our map type to standard.
        mapView.mapType = .standard
        
        // Show user location
        mapView.showsUserLocation = true
        
        return mapView
    }
    
    // MARK: UPDATE MAP
    func updateUIView(_ uiView: MKMapView, context: Context) {
        updateMapType(uiView)
    }
    
    // Update our map type on selection.
    private func updateMapType(_ uiView: MKMapView) {
        switch mapSettings.mapType {
        case 0:
            if #available(iOS 16.0, *) {
                let config = MKStandardMapConfiguration(elevationStyle: elevationStyle(), emphasisStyle: emphasisStyle())
                config.pointOfInterestFilter = MKPointOfInterestFilter.excludingAll
                config.showsTraffic = false
            } else {
                // Fallback on earlier versions
            }
        case 1:
            if #available(iOS 16.0, *) {
                uiView.preferredConfiguration = MKHybridMapConfiguration(elevationStyle: elevationStyle())
            } else {
                // Fallback
            }
        case 2:
            if #available(iOS 16.0, *) {
                uiView.preferredConfiguration = MKImageryMapConfiguration(elevationStyle: elevationStyle())
            } else {
                // Fallback on earlier versions
            }
        default:
            break
        }
    }
    
    // Set the elevation style.
    @available(iOS 16.0, *)
    private func elevationStyle() -> MKMapConfiguration.ElevationStyle {
        if mapSettings.showElevation == 0 {
            return MKMapConfiguration.ElevationStyle.realistic
        } else {
            return MKMapConfiguration.ElevationStyle.flat
        }
    }
    
    // Set the emphasis style.
    @available(iOS 16.0, *)
    private func emphasisStyle() -> MKStandardMapConfiguration.EmphasisStyle {
        if mapSettings.showEmphasisStyle == 0 {
            return MKStandardMapConfiguration.EmphasisStyle.default
        } else {
            return MKStandardMapConfiguration.EmphasisStyle.muted
        }
    }
}

Then I have my MapDisplaySheetView, which contains the following buttons:
enter image description here

Here is the code that I am using:

struct MapDisplaySheetView: View {
    
    @ObservedObject var mapSettings = MapSettings()
   
    @Environment(\.dismiss) var dismiss

    @State var mapType = 0
    @State var showElevation = 0
    @State var showEmphasis = 0
    @State var mapDisplay: [String] = [
        "Standard",
        "Hybrid",
        "Image",
    ]
    @State var mapElevation: [String] = [
        "Realistic",
        "Flat",
    ]
    @State var mapEmphasis: [String] = [
        "Default",
        "Muted",
    ]
    
    var body: some View {
        VStack(spacing: 0) {
            
            // MARK: MapType
            HStack {
                ForEach(mapDisplay, id: \.self) { item in
                    VStack {
                        HStack {
                            VStack {
                                Button(action: {
                                    switch item {
                                      case "Standard": mapSettings.mapType = 0
                                      case "Hybrid": mapSettings.mapType = 1
                                      case "Image": mapSettings.mapType = 2
                                      default: mapSettings.mapType = 0
                                    }
                                    print("User has selected \(item) map type.")
                                }, label: {
                                    ZStack {
                                        Text(item)
                                            .multilineTextAlignment(.center)
                                    }
                                }) //: Button
                            } //: VStack
                        } //: HStack
                    }
                    .onChange(of: mapType) { newValue in
                        mapSettings.mapType = newValue
                        log.info("The new map type is: \(newValue)")
                    }
                } //: ForEach
            } //: HStack
            
            // MARK: Map Elevation
            HStack {
                
                ForEach(mapElevation, id: \.self) { item in
                    VStack {
                        HStack {
                            VStack {
                                Button(action: {
                                    switch item {
                                      case "Realistic": mapSettings.showElevation = 0
                                      case "Flat": mapSettings.showElevation = 1
                                      default: mapSettings.showElevation = 0
                                    }
                                    print("User has selected \(item) map elevation.")
                                }, label: {
                                    ZStack {
                                        Text(item)
                                            .multilineTextAlignment(.center)
                                    }
                                }) //: Button
                            }
                        }
                    }
                }
                
                ForEach(mapEmphasis, id: \.self) { item in
                    VStack {
                        HStack {
                            VStack {
                                Button(action: {
                                    switch item {
                                      case "Default": mapSettings.showEmphasisStyle = 0
                                      case "Muted": mapSettings.showEmphasisStyle = 1
                                      default: mapSettings.showEmphasisStyle = 0
                                    }
                                    print("User has selected \(item) map emphasis.")
                                }, label: {
                                    ZStack {
                                        Text(item)
                                            .multilineTextAlignment(.center)
                                    }
                                }) //: Button
                            }
                        }
                    }
                }
            } //: HStack
        }
    }
}

// Mapping
final class MapSettings: ObservableObject {
    @Published var mapType = 0
    @Published var showElevation = 0
    @Published var showEmphasisStyle = 0
}

I am attempting to use case 0 for the top 3 buttons, which are the mapType and then case 1 for the bottom 2 left buttons and then case 2 for the bottom 2 right buttons, but I can't seem to get the map to update at all, which I believe there is an issue inside MapViewUIKit and got the mapType is set.

Could the issue be with mapView.mapType = .standard?

I am using this guide as an example: https://holyswift.app/new-mapkit-configurations-with-swiftui/


Solution

  • First, change your settings to a struct (you don't need a reference type in this situation):

    struct MapSettings {
        var mapType = 0
        var showElevation = 0
        var showEmphasisStyle = 0
    }
    

    Then fix the @ObservedObject to:

    @State var mapSettings = MapSettings()
    

    Then you can do

    struct MapViewUIKit: UIViewRepresentable {
        let mapSettings: MapSettings
    

    updateUIView will be called when mapSettings has changed from the last time this View was init, and then you can use the new values to make any changes to MKMapView if required.

    Another mistake is Coordinator(self), self is just a value which is immediately discarded after SwiftUI updates so that won't work, try this structure instead:

    struct MyMapView: 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)
            }
        }
    }