我一直在围绕
MKMapView
开发 SwiftUI 包装器,并在尝试处理地图可见区域的绑定值时,我创建了一个自定义属性包装器,允许 MKMapViewDelegate
发送当前可见区域,同时仅通过特殊函数接受传入值。我这样做是因为,如果我尝试使用简单的 Binding<MKMapRect>
来设置可见地图区域并读取它,则会导致问题(在地图视图运动时,两者会发生冲突)。
这里有足够的代码来为任何有兴趣并且在制作 SwiftUI 包装时遇到类似问题的人提供工作示例
MKMapView
。我的问题在底部...
首先,精美的、包装好的 MapView:
struct FancyMapView: UIViewRepresentable {
private var coordinateBinding: CoordinateBinding?
init(coordinateBinding: CoordinateBinding? = nil) {
self.coordinateBinding = coordinateBinding
}
func makeUIView(context: Context) -> MKMapView {
let view = MKMapView()
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: MKMapView, context: Context) {
// If coordinateBinding was used to request a new center coordinate...
if let (requestedCoord, animate) = coordinateBinding?.externalSetValue {
// send the view to the coordinate, and remove the request
uiView.setCenter(requestedCoord, animated: animate)
DispatchQueue.main.async {
coordinateBinding?.externalSetValue = nil
}
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MKMapViewDelegate {
var parent: FancyMapView
init(_ parent: FancyMapView) {
self.parent = parent
}
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
// Send the mapView's current centerCoordinate to parent's binding
DispatchQueue.main.async {
self.parent.coordinateBinding?.wrappedValue = mapView.centerCoordinate
}
}
}
}
接下来,属性包装器
CoordinateBinding
,它与FancyMapView
位于同一文件中,因此可以使用fileprivate
访问。
@propertyWrapper struct CoordinateBinding: DynamicProperty {
// fileprivate setters so only CoordinateBinding and FancyMapView can set these values
@State public fileprivate(set) var wrappedValue: CLLocationCoordinate2D?
@State fileprivate var externalSetValue: (CLLocationCoordinate2D, Bool)?
// public func called to set the MapView's center coordinate
public func sendTo(coordinate: CLLocationCoordinate2D, animated: Bool) {
externalSetValue = (coordinate, animated)
}
}
还有一个使用
ContentView
的 FancyMapView
:
struct ContentView: View {
@CoordinateBinding var centerCoordinate: CLLocationCoordinate2D? = nil
@StateObject var viewModel = MapViewModel()
var body: some View {
FancyMapView(coordinateBinding: _centerCoordinate)
.overlay(alignment: .top) {
VStack {
Text(coordinateString)
Button("Jump!", action: jumpButtonPressed)
}
}
}
var coordinateString: String {
if let centerCoordinate {
return "\(centerCoordinate.latitude),\(centerCoordinate.longitude)"
} else {
return ""
}
}
func jumpButtonPressed() {
let coord = viewModel.nextSampleCoordinate()
_centerCoordinate.sendTo(coordinate: coord, animated: true)
}
}
还有一个简单的 ViewModel 类:
class MapViewModel: ObservableObject {
var sampleIndex = 0
var sampleCoordinates: [CLLocationCoordinate2D] {
[
CLLocationCoordinate2D(latitude: 51.49863, longitude: -0.07518), // london
CLLocationCoordinate2D(latitude: 40.71342, longitude: -73.98839), // new york
CLLocationCoordinate2D(latitude: -37.90055, longitude: 144.98106) // melbourne
]
}
func nextSampleCoordinate() -> CLLocationCoordinate2D {
let result = sampleCoordinates[sampleIndex]
if sampleIndex == sampleCoordinates.count - 1 {
sampleIndex = 0
} else {
sampleIndex += 1
}
return result
}
}
尽管如此,当我按下按钮时,我的地图视图会从一个中心坐标跳转到另一个中心坐标,如果地图仍在以动画方式移动到前一个坐标,它只会改变路线并在正确的位置结束。 (显然,这一切都可以通过原生 SwiftUI
Map
视图来完成,但这是我正在研究的简化版本,所以请在这里幽默一下)
如果我想通过将包装的
ContentView
以及设置坐标的代码移动到 @CoordinateBinding
中来简化 MapViewModel
文件,就会出现问题。我找不到一种方法可以做到这一点并且功能仍然有效。
如果我把它作为属性放在
MapViewModel
中:
@CoordinateBinding var coordinateBinding: CLLocationCoordinate2D?
然后
ContentView
无法访问底层 CoordinateBinding
结构体以输入到 FancyMapView
初始化器中。
如果我在
MapViewModel
中使用此属性:
var coordinateBinding = CoordinateBinding()
然后
ContentView
不会从 coordinateBinding
读取更新。如果我创建该属性,结果相同@Published
。
如果我必须将包装的属性保留在视图而不是 ViewModel 中,那并不是世界末日,但如果有人知道实现此目的的方法,我很乐意听到。谢谢!
问题是由于 UIViewReprestable 中的一些错误造成的,例如
Coordinator(self)
应该是
Coordinator()
self 是结构体的值,它在 init 中多次出现,因此它已经过时了。
此外,您不能在值类型中使用dispatchAsync,并且您的更新需要像这样使用绑定的新值
@Binding var coordinate: CLLocationCoordinate2D
func updateUIView(_ uiView: MKMapView, context: Context) {
context.coordinator.centerDidChange = nil // prevents update loop
uiView.centerCoordinate = coordinate
context.coordinator.centerDidChange = c in {
coordinate = c
}
}
现在您可以删除视图模型对象,因为这是视图结构和状态/绑定的工作。
您的协调器需要您从地图委托函数调用的 centerDidChange 闭包。