SwiftUI 视图模型中的自定义属性包装器

问题描述 投票:0回答:1

我一直在围绕

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 中,那并不是世界末日,但如果有人知道实现此目的的方法,我很乐意听到。谢谢!

swiftui mapkit property-wrapper
1个回答
0
投票

问题是由于 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 闭包。

© www.soinside.com 2019 - 2024. All rights reserved.