Search code examples
swiftvariablesswiftuipropertiesgeolocation

Passing data from UIViewRepresentable function to SwiftUI View


The user looks for the delivery address on the map, then the address is identified by the marker located in the middle of the screen. And then the address is obtained through this marker. How to display an address in the user interface ?

struct MapView: UIViewRepresentable {

@Binding var centerCoordinate: CLLocationCoordinate2D

var currentLocation: CLLocationCoordinate2D?
var withAnnotation: MKPointAnnotation?

class Coordinator: NSObject, MKMapViewDelegate {
    
    var parent: MapView
    var addressLabel: String = "222"
    
    init(_ parent: MapView) {
        self.parent = parent
    }
    
    func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
        if !mapView.showsUserLocation {
            parent.centerCoordinate = mapView.centerCoordinate
        }
    }
    
    ...
    
    func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
        
        let center = getCenterLocation(for: mapView)
        let geoCoder = CLGeocoder()
        
        geoCoder.reverseGeocodeLocation(center) { [weak self] (placemarks, error) in
            guard let self = self else { return }
            
            if let _ = error {
                //TODO: Show alert informing the user
                print("error")
                return
            }
            
            guard let placemark = placemarks?.first else {
                //TODO: Show alert informing the user
                return
            }
            
            let streetNumber = placemark.subThoroughfare ?? ""
            let streetName = placemark.thoroughfare ?? ""
            
            DispatchQueue.main.async {
                self.addressLabel =  String("\(streetName) | \(streetNumber)")
                print(self.addressLabel)
                
            }
        }
    }
}

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

func makeUIView(context: Context) -> MKMapView {
    let mapView = MKMapView()
    mapView.delegate = context.coordinator
    mapView.showsUserLocation = false
    return mapView
}

func updateUIView(_ uiView: MKMapView, context: Context) {
    if let currentLocation = self.currentLocation {
        if let annotation = self.withAnnotation {
            uiView.removeAnnotation(annotation)
        }
        uiView.showsUserLocation = true
        let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
        uiView.setRegion(region, animated: true)
    } else if let annotation = self.withAnnotation {
        uiView.removeAnnotations(uiView.annotations)
        uiView.addAnnotation(annotation)
        }
    }
}

I am trying to pass the address to the UI. What's the most correct way to do this? In the interface, I want to get the address from an ever-changing variable addressLabel

import SwiftUI
import MapKit

fileprivate let locationFetcher = LocationFetcher()

struct LocationView: View {

@State var centerCoordinate = CLLocationCoordinate2D()
@State var currentLocation: CLLocationCoordinate2D?
@State var annotation: MKPointAnnotation?

var body: some View {
    ZStack {
    
        MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation)
            .edgesIgnoringSafeArea([.leading, .trailing, .bottom])
            .onAppear(perform: {
                locationFetcher.start()
            })
    }
    .overlay(
    
        ZStack {


            Text("\(*MapView(centerCoordinate: $centerCoordinate, currentLocation: currentLocation, withAnnotation: annotation).makeCoordinator().addressLabel OMG????*)")
            
                .offset(y: 44)
        }
    
    )
}

struct LocationView_Previews: PreviewProvider {
    static var previews: some View {
        LocationView()
    }
}

How can i do this ?

Thanks in advance


Solution

  • Here is one approach. Have a single source of truth that both UIKit and SwiftUI can access.

    @available(iOS 15.0, *)
    struct LocationView: View {
        //It is better to have one source of truth
        @StateObject var vm: MapViewModel = MapViewModel()
        
        var body: some View {
            ZStack {
                MapView(vm: vm)
                    .edgesIgnoringSafeArea([.leading, .trailing, .bottom])
                    .onAppear(perform: {
                        //locationFetcher.start() //No Code provided
                    })
            }
            .overlay(
                HStack{
                    Spacer()
                    Text(vm.addressLabel)
                    Spacer()
                    //Using offset is subjective since screen sizes change just center it
                }
                
                
            )
            //Sample alert that adapts to what is
            .alert(isPresented: $vm.errorAlert.isPresented, error: vm.errorAlert.error, actions: {
                
                if vm.errorAlert.defaultAction != nil{
                    Button("ok", role: .none, action: vm.errorAlert.defaultAction!)
                }
                
                if vm.errorAlert.cancelAction != nil{
                    Button("cancel", role: .cancel, action: vm.errorAlert.cancelAction!)
                }
                
                if vm.errorAlert.defaultAction == nil && vm.errorAlert.cancelAction == nil {
                    Button("ok", role: .none, action: {})
                }
            })
        }
    }
    //UIKit and SwiftUI will have access to this ViewModel so all the data can have one souce of truth
    class MapViewModel: ObservableObject{
        //All the variables live here
        @Published  var addressLabel: String = "222"
        @Published var centerCoordinate: CLLocationCoordinate2D = CLLocationCoordinate2D()
        
        @Published var currentLocation: CLLocationCoordinate2D? = nil
        @Published var withAnnotation: MKPointAnnotation? = nil
        @Published var annotation: MKPointAnnotation?
        //This tuple variable allows you to have a dynamic alert in the view
        @Published var errorAlert: (isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?) = (false, MapErrors.unknown, nil, nil)
        //The new alert requires a LocalizedError
        enum MapErrors: LocalizedStringKey, LocalizedError{
            case unknown
            case failedToRetrievePlacemark
            case failedToReverseGeocode
            case randomForTestPurposes
            //Add localizable.strings to you project and add these keys so you get localized messages
            var errorDescription: String?{
                switch self{
                    
                case .unknown:
                    return "unknown".localizedCapitalized
                case .failedToRetrievePlacemark:
                    return "failedToRetrievePlacemark".localizedCapitalized
                    
                case .failedToReverseGeocode:
                    return "failedToReverseGeocode".localizedCapitalized
                    
                case .randomForTestPurposes:
                    return "randomForTestPurposes".localizedCapitalized
                    
                }
            }
        }
        //Presenting with this will ensure that errors keep from getting lost by creating a loop until they can be presented
        func presentError(isPresented: Bool, error: MapErrors, defaultAction: (() -> Void)?, cancelAction: (() -> Void)?, count: Int = 1){
            //If there is an alert already showing
            if errorAlert.isPresented{
                //See if the current error has been on screen for 10 seconds
                if count >= 10{
                    //If it has dismiss it so the new error can be posted
                    errorAlert.isPresented = false
                }
                //Call the method again in 1 second
                DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
                    let newCount = count + 1
                    self.presentError(isPresented: isPresented, error: error, defaultAction: defaultAction, cancelAction: cancelAction, count: newCount)
                }
            }else{
                errorAlert = (isPresented, error, defaultAction, cancelAction)
            }
        }
        
    }
    struct MapView: UIViewRepresentable {
        @ObservedObject var vm: MapViewModel
        
        class Coordinator: NSObject, MKMapViewDelegate {
            var parent: MapView
            
            init(_ parent: MapView) {
                self.parent = parent
            }
            
            func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
                if !mapView.showsUserLocation {
                    parent.vm.centerCoordinate = mapView.centerCoordinate
                }
            }
            
            
            func mapView(_ mapView: MKMapView, regionDidChangeAnimated animated: Bool){
                getAddress(center: mapView.centerCoordinate)
                //Just to demostrate the error
                //You can remove this whenever
    #if DEBUG
                if Bool.random(){
                    self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.randomForTestPurposes, defaultAction: nil, cancelAction: nil)
                    
                }
    #endif
                
            }
            //Gets the addess from CLGeocoder if available
            func getAddress(center: CLLocationCoordinate2D){
                let geoCoder = CLGeocoder()
                
                geoCoder.reverseGeocodeLocation(CLLocation(latitude: center.latitude, longitude: center.longitude)) { [weak self] (placemarks, error) in
                    guard let self = self else { return }
                    
                    if let _ = error {
                        //TODO: Show alert informing the user
                        print("error")
                        self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToReverseGeocode, defaultAction: nil, cancelAction: nil)
                        return
                    }
                    
                    guard let placemark = placemarks?.first else {
                        //TODO: Show alert informing the user
                        self.parent.vm.presentError(isPresented: true, error: MapViewModel.MapErrors.failedToRetrievePlacemark, defaultAction: nil, cancelAction: nil)
                        return
                    }
                    
                    let streetNumber = placemark.subThoroughfare ?? ""
                    let streetName = placemark.thoroughfare ?? ""
                    
                    DispatchQueue.main.async {
                        self.parent.vm.addressLabel =  String("\(streetName) | \(streetNumber)")
                        print(self.parent.vm.addressLabel)
                        
                    }
                }
            }
        }
        
        func makeCoordinator() -> Coordinator {
            Coordinator(self)
        }
        
        func makeUIView(context: Context) -> MKMapView {
            let mapView = MKMapView()
            mapView.delegate = context.coordinator
            mapView.showsUserLocation = false
            return mapView
        }
        
        func updateUIView(_ uiView: MKMapView, context: Context) {
            if let currentLocation = vm.currentLocation {
                if let annotation = vm.withAnnotation {
                    uiView.removeAnnotation(annotation)
                }
                uiView.showsUserLocation = true
                let region = MKCoordinateRegion(center: currentLocation, latitudinalMeters: 1000, longitudinalMeters: 1000)
                uiView.setRegion(region, animated: true)
            } else if let annotation = vm.withAnnotation {
                uiView.removeAnnotations(uiView.annotations)
                uiView.addAnnotation(annotation)
            }
        }
    }