将数据从 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 用于自定义组件不起作用