'Change the mapType to .satellite etc with a picker

I want to be able to change the mapType from .standard to .satellite and .hybrid in xCode 13.3 Can anybody tell me if it is at all possible with this code? I implemented a picker to do the job but unfortunately I could not make it work. I succeeded making it change with different code but then buttons map + and map - would not work anymore

import Foundation
import SwiftUI
import MapKit

struct QuakeDetail: View {
    var quake: Quake
    
    @State private var region : MKCoordinateRegion
    init(quake : Quake) {
        self.quake = quake
        _region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
                                                         span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
    }
    
    @State private var mapType: MKMapType = .standard
    
    var body: some View {
        
        VStack {
                Map(coordinateRegion: $region, annotationItems: [quake]) { item in
                    MapMarker(coordinate: item.coordinate, tint: .red)
                } .ignoresSafeArea()
                HStack {
                    Button {
                        region.span.latitudeDelta *= 0.5
                        region.span.longitudeDelta *= 0.5
                    } label: {
                        HStack {
                            Text("map")
                            Image(systemName: "plus")
                        }
                    }.padding(5)//.border(Color.blue, width: 1)
                    Spacer()
                    QuakeMagnitude(quake: quake)
                    Spacer()
                    Button {
                        region.span.latitudeDelta /= 0.5
                        region.span.longitudeDelta /= 0.5
                    } label: {
                        HStack {
                            Text("map")
                            Image(systemName: "minus")
                        }
                    }                    
                }.padding(.horizontal)
                Text(quake.place)
                    .font(.headline)
                    .bold()
                Text("\(quake.time.formatted())")
                    .foregroundStyle(Color.secondary)
                Text("\(quake.latitude)   \(quake.longitude)")
            VStack {
                           Picker("", selection: $mapType) {
                                Text("Standard").tag(MKMapType.standard)
                                Text("Satellite").tag(MKMapType.satellite)
                                Text("Hybrid").tag(MKMapType.hybrid)
        
                            }
                            .pickerStyle(SegmentedPickerStyle())
                            .font(.largeTitle)
                        }
        }
    }
}

Here is the code that changes the mapType but the buttons do not work anymore:

import Foundation
import SwiftUI
import MapKit

struct QuakeDetail: View {
    var quake: Quake
    
    @State private var region : MKCoordinateRegion
    init(quake : Quake) {
        self.quake = quake
        _region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
                                                         span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
    }
    @State private var mapType: MKMapType = .standard
    
    var body: some View {
        
        VStack {
            MapViewUIKit(region: region, mapType: mapType)
                .edgesIgnoringSafeArea(.all)
            HStack {
                Button {
                    region.span.latitudeDelta *= 0.5
                    region.span.longitudeDelta *= 0.5
                } label: {
                    HStack {
                        Text("map")
                        Image(systemName: "plus")
                    }
                }.padding(5)//.border(Color.blue, width: 1)
                Spacer()
                QuakeMagnitude(quake: quake)
                Spacer()
                Button {
                    region.span.latitudeDelta /= 0.5
                    region.span.longitudeDelta /= 0.5
                } label: {
                    HStack {
                        Text("map")
                        Image(systemName: "minus")
                    }
                }
            }.padding(.horizontal)
            Text(quake.place)
                .font(.headline)
                .bold()
            Text("\(quake.time.formatted())")
                .foregroundStyle(Color.secondary)
            Text("\(quake.latitude)   \(quake.longitude)")
            Picker("", selection: $mapType) {
                Text("Standard").tag(MKMapType.standard)
                Text("Satellite").tag(MKMapType.satellite)
                Text("Hybrid").tag(MKMapType.hybrid)
                //Text("Hybrid flyover").tag(MKMapType.hybridFlyover)
            }
            .pickerStyle(SegmentedPickerStyle())
            .font(.largeTitle)
        }
    }
}

struct MapViewUIKit: UIViewRepresentable {
    
    let region: MKCoordinateRegion
    let mapType : MKMapType
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.setRegion(region, animated: false)
        mapView.mapType = mapType
        
        return mapView
    }
        
    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.mapType = mapType
    }
}

I implemented the code and now the buttons work and the mapType changes correctly, thank you very much also for the pointers to the documentation. Unfortunately the annotations do not display the pin at the earthquake location. I changed the title from London to quake.place and the coordinate to coordinate: CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta) but it made no difference. Here are my changes:

import SwiftUI
import MapKit

struct QuakeDetail: View {
    var quake: Quake
    
    @State private var region : MKCoordinateRegion
    init(quake : Quake) {
        self.quake = quake
        _region = State(wrappedValue: MKCoordinateRegion(center: quake.coordinate,
                                                         span: MKCoordinateSpan(latitudeDelta: 10, longitudeDelta: 10)))
    }
    @State private var mapType: MKMapType = .standard
    
    var body: some View {
        
        VStack {

            MapViewUIKit(
                region: $region,
                mapType: mapType,
                annotation: Annotation(
                    title: quake.place,
                    coordinate: CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta)
                ) // annotation
            ).ignoresSafeArea() // MapViewUIKit
            Spacer()
            HStack {
                Button {
                    region.span.latitudeDelta *= 0.5
                    region.span.longitudeDelta *= 0.5
                } label: {
                    HStack {
                        Image(systemName: "plus")
                    }
                }//.padding(5)
                Spacer()
                QuakeMagnitude(quake: quake)
                Spacer()
                Button {
                    region.span.latitudeDelta /= 0.5
                    region.span.longitudeDelta /= 0.5
                } label: {
                    HStack {
                        Image(systemName: "minus")
                    }
                }
            }.padding(.horizontal) // HStack + - buttons and quake magnitude
            
            Text(quake.place)
            Text("\(quake.time.formatted())")
                .foregroundStyle(Color.secondary)
            Text("\(quake.latitude)   \(quake.longitude)")
                .padding(.bottom, -5)
            Picker("", selection: $mapType) {
                Text("Standard").tag(MKMapType.standard)
                Text("Satellite").tag(MKMapType.satellite)
                Text("Hybrid").tag(MKMapType.hybrid)
            }
            .pickerStyle(SegmentedPickerStyle())
            
        }

    }
}

struct Annotation {
    let pointAnnotation: MKPointAnnotation

    init(title: String, coordinate: CLLocationCoordinate2D) {
        pointAnnotation = MKPointAnnotation()
        pointAnnotation.title = title
        pointAnnotation.coordinate = coordinate
    }
}

struct MapViewUIKit: UIViewRepresentable {

    @Binding var region: MKCoordinateRegion
    let mapType : MKMapType
    let annotation: Annotation

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.setRegion(region, animated: false)
        mapView.mapType = mapType

        // Set the delegate so that we can listen for changes and
        // act appropriately
        mapView.delegate = context.coordinator

        // Add the annotation to the map
        mapView.addAnnotation(annotation.pointAnnotation)
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.mapType = mapType
        // Update your region so that it is now your new region
        mapView.setRegion(region, animated: true)
    }

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

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapViewUIKit

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

        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            // We should handle dequeue of annotation view's properly so we have to write this boiler plate.
            // This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
            // MKAnnotationView from our annotation.
            guard annotation is MKPointAnnotation else { return nil }

            let identifier = "Annotation"
            guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
                let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
                annotationView.canShowCallout = true
                return annotationView
            }

            annotationView.annotation = annotation
            return annotationView
        }

        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            // We need to update the region when the user changes it
            // otherwise when we zoom the mapview will return to its original region
            DispatchQueue.main.async {
                self.parent.region = mapView.region
            }
        }
    }
}


Solution 1:[1]

So to use MKMapView we need to set up the UIViewRepresentable properly. MKMapView has a delegate and as such we need to set the delegate for our mapView. We do this by adding a Coordinator to our UIViewRepresentable

So here is a full working example, it may not be 100% perfect but it shows the general idea of what you can do.

I created my own ContentView because your code was missing several things (such as Quake).

MapViewUIKit takes three parameters.

  • A binding for MKCoordinateRegion, it needs to be a binding as we will be passing data back to the ContentView
  • The mapType which is a MKMapType, this is for changing the map type
  • An annotation, this is a custom Annotation type that is used to hold the information about the annotation we wish to show on the map.
struct ContentView: View {

    @State private var region = MKCoordinateRegion(
        center: CLLocationCoordinate2D(
            latitude: 51.507222,
            longitude: -0.1275),
        span: MKCoordinateSpan(latitudeDelta: 0.5, longitudeDelta: 0.5)
    )

    @State private var mapType: MKMapType = .standard

    var body: some View {
        VStack {
            Picker("", selection: $mapType) {
                Text("Standard").tag(MKMapType.standard)
                Text("Satellite").tag(MKMapType.satellite)
                Text("Hybrid").tag(MKMapType.hybrid)
            }
            .pickerStyle(SegmentedPickerStyle())

            MapViewUIKit(
                region: $region,
                mapType: mapType,
                annotation: Annotation(
                    title: "London",
                    coordinate: CLLocationCoordinate2D(latitude: 51.507222, longitude: -0.1275)
                )
            )

            HStack {
                Button {
                    region.span.latitudeDelta *= 0.5
                    region.span.longitudeDelta *= 0.5
                } label: {
                    HStack {
                        Image(systemName: "plus")
                    }
                }.padding(5)

                Button {
                    region.span.latitudeDelta /= 0.5
                    region.span.longitudeDelta /= 0.5
                } label: {
                    HStack {
                        Image(systemName: "minus")
                    }
                }.padding(5)
            }
        }

    }
}

This is the Annotation struct that I created to hold the information about the annotation that we wish to display.

struct Annotation {
    let pointAnnotation: MKPointAnnotation

    init(title: String, coordinate: CLLocationCoordinate2D) {
        pointAnnotation = MKPointAnnotation()
        pointAnnotation.title = title
        pointAnnotation.coordinate = coordinate
    }
}

Finally we need the UIViewRepresentable to tie it all together. I've commented in the code to show what it does.

struct MapViewUIKit: UIViewRepresentable {

    @Binding var region: MKCoordinateRegion
    let mapType : MKMapType
    let annotation: Annotation

    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.setRegion(region, animated: false)
        mapView.mapType = mapType

        // Set the delegate so that we can listen for changes and
        // act appropriately
        mapView.delegate = context.coordinator

        // Add the annotation to the map
        mapView.addAnnotation(annotation.pointAnnotation)
        return mapView
    }

    func updateUIView(_ mapView: MKMapView, context: Context) {
        mapView.mapType = mapType
        // Update your region so that it is now your new region
        mapView.setRegion(region, animated: true)
    }

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

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapViewUIKit

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

        func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
            // We should handle dequeue of annotation view's properly so we have to write this boiler plate.
            // This basically dequeues an MKAnnotationView if it exists, otherwise it creates a new
            // MKAnnotationView from our annotation.
            guard annotation is MKPointAnnotation else { return nil }

            let identifier = "Annotation"
            guard let annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier) else {
                let annotationView = MKMarkerAnnotationView(annotation: annotation, reuseIdentifier: identifier)
                annotationView.canShowCallout = true
                return annotationView
            }

            annotationView.annotation = annotation
            return annotationView
        }

        func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool) {
            // We need to update the region when the user changes it
            // otherwise when we zoom the mapview will return to its original region
            DispatchQueue.main.async {
                self.parent.region = mapView.region
            }
        }
    }
}

This gives the following output

https://imgur.com/a/gH42UED

I would suggest that you familiarise yourself with Apple's documentation and there is a wealth of tutorials out there that can help you.

https://developer.apple.com/documentation/mapkit/mkmapview https://developer.apple.com/documentation/mapkit/mkmapviewdelegate https://www.raywenderlich.com/7738344-mapkit-tutorial-getting-started https://www.hackingwithswift.com/example-code/location/how-to-add-annotations-to-mkmapview-using-mkpointannotation-and-mkpinannotationview https://talk.objc.io/episodes/S01E195-wrapping-map-view

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Andrew