Map Switcher MapKit iOS 14 and up

Hello everyone,

I have just started coding using swift and I´m currently building an app that ist using MapKit. It is required to run on iOS 14 and newer and I want to add a Map switcher to switch between the Map Views Standard, Satellite, Hybrid and eventually also OSM. However this apparently is not as straight forward as it seems and I just don't get it to work. I had multiple attempts such as these two, each interface with a separate MapSwitcherView that open on the press of a button:

var body: some View { ZStack(alignment: .bottomTrailing) { Group { if selectedMapStyle == .openStreetMap { openStreetMapView() } else { MapContainer(region: $locationManager.region, tracking: $tracking, style: selectedMapStyle) } } .id(selectedMapStyle) .onChange(of: selectedMapStyle) { newStyle in print("Style changed to: (newStyle)") }

and

Group { switch selectedMapStyle { case .standard: Map(coordinateRegion: $locationManager.region, interactionModes: .all, showsUserLocation: true, userTrackingMode: $tracking) .id("standard")

case .satellite:
    Map(coordinateRegion: $locationManager.region,
        interactionModes: .all,
        showsUserLocation: true,
        userTrackingMode: $tracking)
    .id("satellite")

case .hybrid:
    Map(coordinateRegion: $locationManager.region,
        interactionModes: .all,
        showsUserLocation: true,
        userTrackingMode: $tracking)
    .id("hybrid")

case .openStreetMap:
    openStreetMapView()
}

}

Unfortunately the map just doesn't switch. Do you have any suggestions? Should I post some more code of the MapSwitcher or something?

Thanks and best regards

Hello and welcome to Swift and the forums!

In your code snippets, the map doesn't actually switch styles because the SwiftUI Map view in iOS 14 doesn't support changing map types out of the box. The id modifier won't magically change the underlying style — it only forces a re-render if the ID is truly different.

The mapStyle(_:) modifier only exists from iOS 17 onwards, so no luck if you need to support iOS 14.

To properly support different map types (standard, satellite, hybrid) on iOS 14, you'll need to drop down to UIKit and use MKMapView via UIViewRepresentable.

Here's a stripped-down example to get you started:

struct MapView: UIViewRepresentable {
    @Binding var coordinateRegion: MKCoordinateRegion
    let style: MKMapType

    func makeUIView(context: Context) -> MKMapView {
        let map = MKMapView()
        map.showsUserLocation = true
        map.delegate = context.coordinator

        return map
    }

    func updateUIView(_ map: MKMapView, context: Context) {
        map.region = coordinateRegion
        map.mapType = style
    }

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

    class Coordinator: NSObject, MKMapViewDelegate {
        private let parent: MapView

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

        func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
            parent.coordinateRegion = mapView.region
        }
    }
}

Then in your SwiftUI view:

if case .openStreetMap = selectedMapStyle {
    openStreetMapView()
} else {
    MapView(coordinateRegion: $locationManager.region, style: selectedMapStyle)
}


Hope this helps. If you have any questions or extra context about your current code, just shout.

Hello, thank you so much for your reply and help. I have actually already tried switching to MKMapView, but that would unfortunately break some other things such as reliable user following/tracking and the automated shifting between it being enabled on a button press and disabled when dragging the Map around. I know that apps even pre iOS 14 did that properly, for example I got an iPhone 7 with iOS 12 running an older, still supported version of the Geocaching app which has map switching and proper tracking / user following.

For context here's how my tracking works. I basically got a Location Manager class with the needed functions that looks like this (plus a few extra functions like getting authorization to use the location that are irrelevant for this problem):

class LocationManager: NSObject, ObservableObject, CLLocationManagerDelegate {
    private let locationManager = CLLocationManager()
    private var hasCenteredOnUser = false
    
    @Published var authorizationStatus: CLAuthorizationStatus = .notDetermined
    // Centre Germany
    @Published var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 51,
            longitude: 10.5),
        span: MKCoordinateSpan(
            latitudeDelta: 9,
            longitudeDelta: 9))
    
    override init() {
        super.init()
        locationManager.delegate = self
        locationManager.desiredAccuracy = kCLLocationAccuracyBest
        locationManager.requestWhenInUseAuthorization()
        locationManager.startUpdatingLocation()
    }
    
    func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
        guard let location = locations.last else { return }
        if !hasCenteredOnUser {
            DispatchQueue.main.async {
                self.region = MKCoordinateRegion(
                    center: location.coordinate,
                    span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05)
                )
                self.hasCenteredOnUser = true
            }
        }
    }
 
    //For the button to get the location
    func getCurrentLocation() -> CLLocationCoordinate2D? {
        return locationManager.location?.coordinate
    }
}

Honestly I can't really believe that ma switching is not possible in MapKit for SwiftUI for iOS 14, that would be crazy as it is such a simple function. You said that it doesn't re-render because it needs to be truly different. But why isn't it? Can't we maybe figure out a solution there to force it to re-render?

As I said, thanks so much already.

The main point here is that to be able to change the style of a map in iOS 14, you must use MKMapView.

Another thing to note is that SwiftUI's Map and MKMapView actually rely on the same underlying technologies. The key difference is that MKMapView (UIKit) gives you much more granular control, whereas the SwiftUI Map is a higher-level, declarative wrapper that, in iOS 14, simply doesn't expose the needed map style API. Remember, SwiftUI was only a year old at that point, so it didn't have half the features it does today, including proper integration with other frameworks.

Regarding tracking and user-following: you can definitely replicate this behaviour using MKMapView. It just takes a bit more code, but a quick skim through the documentation will point you in the right direction. You can make use of view properties and delegate methods to handle your tracking logic as needed, as well as any additional features you want to implement. For example, you can set userTrackingMode to .follow or .followWithHeading, and react to user interactions by implementing the mapView(_:regionWillChangeAnimated:) and mapView(_:regionDidChangeAnimated:) delegate methods.

In other words, all the "automated" stuff SwiftUI does under the hood is still possible — you just need to handle it yourself imperatively in UIKit. Once you wrap that up neatly in your UIViewRepresentable, you'll have full control over style switching and user interaction, without giving up tracking.

I know it sounds a bit tedious, but that's unfortunately the reality with iOS 14 and SwiftUI's early map support.



The focus of this post is on how to change the map style of Map in iOS 14, and I believe I've covered that question here. If you have other issues, I suggest creating a new post to keep the topics clean and focused.

Okay, thank you very much, I'll try implementing it that way then.

Map Switcher MapKit iOS 14 and up
 
 
Q