将数据从 UIViewRepresentable 函数传递到 SwiftUI 视图

Posted

技术标签:

【中文标题】将数据从 UIViewRepresentable 函数传递到 SwiftUI 视图【英文标题】:Passing data from UIViewRepresentable function to SwiftUI View 【发布时间】:2021-11-23 15:26:40 【问题描述】:

用户在地图上查找送货地址,然后地址由位于屏幕中间的标记标识。然后通过这个标记获得地址。如何在用户界面中显示地址?

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)
        
    

我正在尝试将地址传递给 UI。 最正确的方法是什么? 在界面中,我想从一个不断变化的变量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()
    

我该怎么做?

提前致谢

【问题讨论】:

没有Minimal Reproducible Example 无法帮助您解决问题。 一般来说,如果UIVIewRepresentable 需要从表示的UIView 更新状态,您会使用绑定。在您的情况下,您有一个Coordinator,它还充当代表UIView 的代表,我看不到它会更新您的SwiftUI 视图绑定。因此,也许您想在 mapView(_:regionDidChangeAnimated:) 方法中执行此操作。 【参考方案1】:

这是一种方法。拥有 UIKit 和 SwiftUI 都可以访问的单一事实来源。

@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)
        
    

【讨论】:

以上是关于将数据从 UIViewRepresentable 函数传递到 SwiftUI 视图的主要内容,如果未能解决你的问题,请参考以下文章

将 UIViewRepresentable 连接到 SwiftUI

Swiftui - 从 UIViewRepresentable 访问 UIKit 方法/属性

MapView:UIViewRepresentable 不以获取的列表为中心

将 UIViewRepresentable 用于自定义组件不起作用

在 SwiftUI UIViewRepresentable 上刷新 PDFViewer

更新 UIViewRepresentable 中的绑定