Search code examples
iosswiftswiftuiuiviewrepresentable

UIViewRepresentable and Map SwiftUI


I am using UIKit MKMapView in SwiftUI to have more functionality. I pass the map region to the UIViewRepresentable via Binding, so I can update the UIKit view from the SwiftUI view that uses that MKMapView. The issue is that I have a button on top of the map that is responsible for centering the map back to the user location, but every time the map region changes, the updateUIView function triggers, and the map centers it self, which leads to the map being stuck because whenever I move the map (i.e zoom, drag, rotate) the map centers itself.

How can I conditionally check if the button was clicked - and only then center the map?


updateUIView:
func updateUIView(_ uiView: MKMapView, context: Context) {
    if let userLocation = uiView.annotations.first(where: { $0 is MKUserLocation }) {
        uiView.setRegion(MKCoordinateRegion(center: userLocation.coordinate, span: MapDetails.defaultSpan), animated: true)
    } // this gets triggered everytime map region changes.
}

UIViewRepresentable:

struct MapView: UIViewRepresentable {
@Binding var mapRegion: MKCoordinateRegion
@Binding var locations: [Location]
...
}

Main SwiftUI View (Where I use the MKMapView):

struct ExploreView: View {


@StateObject private var viewModel = MapViewModel()
@State private var didAppearOnce = false

@ViewBuilder
var body: some View {
    NavigationView {
        ZStack {
            MapView(mapRegion: $viewModel.mapRegion, locations: $viewModel.locations)
                .accentColor(.pink)
            ...
}

So my question is, is there any way to conditionally check whether the button was pressed and only then center the map inside updateUIView ?


Solution

  • Leave updateUIView empty, and add Combine in your viewModel

    import Combine
    
    class MapViewModel:ObservableObject {
    
    @Published var buttonCenterPressed = false
    var buttonCancellable: AnyCancellable?
    
    }
    

    In UIViewRepresentable add subscriber in makeUIView with mapView in parameters

    struct MapView: UIViewRepresentable {
       @Binding var mapRegion: MKCoordinateRegion
       @Binding var locations: [Location]
       var viewModel: MapViewModel
     ...
    
     func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView(frame: .zero)
        mapView.showsUserLocation = true
        setSubscriber(mapView) 
        return mapView 
    
      }
    
     func updateUIView(_ mapView: MKMapView, context: Context) {}
    
     func setSubscriber(_ mapView: MKMapView){
        viewModel.buttonCancellable = viewModel.$buttonCenterPressed.sink(receiveValue: { _ in
            
            if let userLocation = mapView.annotations.first(where: { $0 is MKUserLocation }) {
                mapView.setRegion(MKCoordinateRegion(center: userLocation.coordinate, span: MapDetails.defaultSpan), animated: true)
            }
                
            
        })
    }
    

    add viewModel in parameter to the MapView

    struct ExploreView: View {
    
    
    @StateObject private var viewModel = MapViewModel()
    @State private var didAppearOnce = false
    
    @ViewBuilder
    var body: some View {
       NavigationView {
        ZStack {
            MapView(mapRegion: $viewModel.mapRegion, locations:   $viewModel.locations, viewModel: viewModel)
                .accentColor(.pink)
            ...
      }
    

    now you can implement action centering button and MapView will be updated.

    Button(action: {
             viewModel.buttonCenterPressed.toggle()
            }, label: {
                Text("Center")
    
            })