SwiftUI:自定义 MKMapView 标注

\color{red}{\Large \mathbf{Hacking \quad with \quad iOS: SwiftUI \quad Edition}}

{\Large \mathbf{Bucket \ List}}

将标注添加到MKMapView只是在正确位置放置标注的问题,但是在此应用中,我们希望用户能够点按位置以获取更多信息,然后再次点按以编辑该位置。要实现所有这些,需要一点 \color{red}{\mathbf{SwiftUI}},一点\color{red}{\mathbf{UIKit}}和一点 \color{red}{\mathbf{MapKit}},它们都融合在一起——这是一个有趣的挑战!

第一步是实现mapView(_:viewFor :)方法,如果我们要提供自定义视图来表示地图标注,则会调用该方法。我们之前已经看过此内容,但是这次我们将使用更高级的解决方案,该解决方案可重复使用视图以提高性能,并添加一个可点击以获取更多信息的按钮。MapKit 以一种奇怪的方式来处理按钮的按下,但这并不难,刚开始时有点奇怪。

无论如何,这里最主要的是重用视图以提高性能。请记住,创建视图的成本很高,因此最好创建少量视图并根据需要对其进行回收——只需更改文本标签,而不是每次都销毁并重新创建它们。

MapKit为我们提供了一个很好而简单的API,用于处理视图重用:我们创建所选择的字符串标识符,然后在地图视图上调用dequeueReusableAnnotationView(withIdentifier :),并传入该标识符。如果有等待回收的视图,我们将其取回并可以根据需要进行重新配置;如果没有,我们将返回nil并需要自己创建视图。

如果确实返回nil,则意味着我们需要创建视图,这意味着实例化一个新的MKPinAnnotationView并将其显示出来。但是,我们还将设置一个名为rightCalloutAccessoryView的属性,在该属性中,我们将放置一个按钮以显示更多信息。

我们不在 SwiftUI 中,这意味着我们无法使用Button视图。相反,我们需要使用UIKit中的UIButton。我可以花几个小时来教您有关使用UIButton的复杂性,但是幸运的是,我不需要:与MapKit一起使用时,它只有一行代码,因为我们可以使用称为.detailDisclosure的内置按钮样式——看起来像是一个带有圆圈的 “i”

UIKitMapKit中的所有委托方法一样,下一个名称很长。因此,最好的办法是进入MapViewCoordinator类,然后键入“viewfor” 以弹出Xcode的代码来完成。希望会弹出正确的MKMapView方法,您可以按回车键以使其填写完整方法。

完成后,对其进行编辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
        // 这是我们视图重用的唯一标识符
        let identifier = "Placemark"

        // 试图找到一个我们可以回收的视图
        var annotationView = mapView.dequeueReusableAnnotationView(withIdentifier: identifier)

        if annotationView == nil {
            // 我们找不到一个;创建一个新的
            annotationView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: identifier)

            // 允许它显示弹出信息
            annotationView?.canShowCallout = true

            // 将信息按钮附加到视图
            annotationView?.rightCalloutAccessoryView = UIButton(type: .detailDisclosure)
        } else {
            // 我们有一个重用的视图,所以给它新的标注
            annotationView?.annotation = annotation
        }

        // 无论是新视图还是复用视图,都将其返回
        return annotationView
    }

那还行不通,因为即使我们将canShowCallout设置为true,MapKit 也不会显示没有标题的注释的标注。我们还没有一种输入标题的方法,所以现在我们只对其中的一个进行硬编码——回到ContentView.swift并添加以下行,我们可以为locations数组创建MKPointAnnotation

1
newLocation.title = "Example location"

如果您现在运行该应用程序,则会发现您可以通过按 + 按钮放置大头针,然后点击大头针以显示标题——以及右侧的小“i”按钮。使按钮发挥作用是乐趣所在,尽管对“乐趣”有非常特定的定义。

事情一开始就很简单:我们将在MapView中添加两个属性,以跟踪我们是否应该显示地点详细信息以及实际选择的地点。它们将构成MKMapViewSwiftUI 之间的另一座桥梁,因此我们将用@Binding标记它们。

1
2
@Binding var selectedPlace: MKPointAnnotation?
@Binding var showingPlaceDetails: Bool

我更喜欢将所有@Binding属性放在一起,但是,这取决于您自己,这会影响 Swift 创建其成员初始化器的方式。

拥有这些额外的属性意味着我们需要调整MapView_Previews结构体以包括它们,如下所示:

1
2
3
4
5
6
MapView(
    centerCoordinate: .constant(MKPointAnnotation.example.coordinate),
    selectedPlace: .constant(MKPointAnnotation.example),
    showingPlaceDetails: .constant(false),
    annotations: [MKPointAnnotation.example]
)

请记住要根据您在MapView中排列属性的方式来调整这些参数的顺序!

在 ContentView.swift 中,我们需要做很多相同的事情,首先需要传递@State属性。因此,首先将它们添加到ContentView

1
2
@State private var selectedPlace: MKPointAnnotation?
@State private var showingPlaceDetails = false

现在,我们可以更新其MapView行以在以下位置传递这些值:

1
2
3
4
5
MapView(centerCoordinate: $centerCoordinate,
        selectedPlace: $selectedPlace,
        showingPlaceDetails: $showingPlaceDetails,
        annotations: locations)
    .edgesIgnoringSafeArea(.all)

showingPlaceDetails布尔值变为true时,我们希望显示一个带有当前所选位置的标题和副标题的警报Alert,以及一个允许用户编辑该位置的按钮。我们还没有准备好进行编辑,但是我们至少可以显示警报并将其连接到 MapKit

首先将以下alert()修饰符添加到ContentViewZStack中:

1
2
3
4
5
6
7
8
.alert(isPresented: $showingPlaceDetails) {
    Alert(title: Text(selectedPlace?.title ?? "Unknown"),
          message: Text(selectedPlace?.subtitle ?? "Missinplace information."),
          primaryButton: .default(Text("OK")),
          secondaryButton: .default(Text("Edit")) {
        // edit this place
    })
}

最后,我们需要更新·MapView·,以便为标注点击“i”按钮来设置selectedPlaceshowingPlaceDetails属性。这是通过实现一个名称比以前更长的方法来完成的,所以最好的办法是进入Coordinator类,然后键入“ mapviewcall” – Xcode的会提供建议,您可以按回车键将其填写。

该方法的重要部分称为calloutAccessoryControlTapped,在点击按钮时会调用此方法,这取决于我们来决定应该发生什么。在这种情况下,我们将首先检查我们是否有一个MKAnnotationView,如果有的话,使用它来设置父MapViewselectedPlace属性。然后,我们还可以将showingPlaceDetails设置为true,这将依次触发ContentView中的警报——这是另一个链接,这次将地图标注接头连接到我们的警报。

现在将此方法添加到Coordinator类中:

1
2
3
4
5
6
7
8
9
func mapView(_ mapView: MKMapView,
               annotationView view: MKAnnotationView,
               calloutAccessoryControlTapped control: UIControl) {

    guard let placemark = view.annotation as? MKPointAnnotation else { return }

    parent.selectedPlace = placemark
    parent.showingPlaceDetails = true
}

完成此步骤后,我们的项目的下一步已完成,因此请立即运行,您应该可以放置图钉,点击它以显示更多信息,然后按``i''按钮以显示警报Alert。这就将他们联系在一起了!

译自 Customizing MKMapView annotations