0

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
            }
        }
    }
}
  • I don’t think that particular feature is exposed to SwiftUI. You might be better off wrapping a Swift MKMapView in a UIViewRepresentable. It would give you greater control but it could make it more complicated to use. Alternatively you could try calling something like `MKMapView.appearance().mapType = .satellite` when you change the map type. Though I am not sure that will work. – Andrew Mar 29 '22 at 06:54
  • Thank you for your answer Andrew. I could not work out how to implement MKMapView.appearance().mapType = .satellite in the code above. Could you please explain how and what to insert where? I would really appreciate it. I tried :Picker("", selection: $mapType) { Text("Standard").tag(MKMapView.appearance().mapType = .satellite) } but I get all sort of errors. – Mariano Giovanni Mario Leonti Mar 29 '22 at 07:20
  • That is not correct, you shouldn't be trying to set something in the tag. The tag is used to track selection. You could try the `onChange` modifier. This question shows how to use it stackoverflow.com/a/63289866/5508175 but unfortunately changing the mapType doesn't re-render the map leaving it on the normal map, it used to work when you set it in the init, however that doesn't work either. I think your best bet is to use a `MKMapView` inside a `UIViewRepresentable` – Andrew Mar 29 '22 at 08:44
  • I tried using a MKMapView and the MapTypes change correctly but the buttons to increase decrease the map zoom do not work anymore. I feel I am in between a rock and a hard place. – Mariano Giovanni Mario Leonti Mar 29 '22 at 09:36
  • You should be able to use MKMapView to do what you want; you will need to re-implement the buttons so that they work in the `UIViewRepresentable`. However you haven't posted the code so it is impossible to see what you have done. https://stackoverflow.com/questions/61598266/swiftui-on-button-tap-call-mkmapview-setregion This is for setting the region (which is what you want to do) so you will need to pass some binding (perhaps the zoom level) to your `UIViewRepresentable `and update the region in the `updateUIView` – Andrew Mar 29 '22 at 12:24
  • Thanks again Andrew. I posted the code. Also I would like to be able to show a pin at the quake location as it appeared before I changed the code to show the map type – Mariano Giovanni Mario Leonti Mar 30 '22 at 21:37

1 Answers1

2

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://i.stack.imgur.com/jZmZg.jpg

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

Andrew
  • 26,706
  • 9
  • 85
  • 101
  • Thank you very much for your patience. I implemented the code you posted and now both type and zoom work on the map. The only think remaining is to change the pin from London to the quake location. I posted the changes that I implemented above but unfortunately I could not make it work. – Mariano Giovanni Mario Leonti Mar 31 '22 at 23:58
  • 1
    You still haven't included `Quake`. You should include all required info in your question. You have made a typo when creating the `Annotation`. You have written the coordinate as `CLLocationCoordinate2D(latitude: region.span.latitudeDelta, longitude: region.span.longitudeDelta)`. You have tied your annotation to the region's latitude and longitude delta. The delta is the distance between the min and max latitude/longitude to be displayed, not an actual coordinate. The annotation should be tied to the Quake's coordinate. Perhaps `quake.coordinate` might work. – Andrew Apr 01 '22 at 07:40
  • 1
    The code works, you just have to pass it a `CLLocationCoordinate2D`. `quake.coordinate` should work. Your annotation should now be `Annotation(title: quake.place, coordinate: quake.coordinate)`, if that isn't working you just have to pass the latitude and longitude from your quake in to the CLLocationCoordinate2D. – Andrew Apr 01 '22 at 07:49
  • Hi Andrew I finally got it to work. This is how: mapType: mapType, annotation: Annotation( title: quake.place, coordinate: CLLocationCoordinate2D(latitude: quake.coordinate.latitude, longitude: quake.coordinate.longitude) ) // annotation. Thanks for all your help – Mariano Giovanni Mario Leonti Apr 01 '22 at 08:12