Home > Back-end >  Passing data from UIViewRepresentable function to SwiftUI View
Passing data from UIViewRepresentable function to SwiftUI View

Time:11-24

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

CodePudding user response:

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 gettting 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)
        }
    }
}
  • Related