SwiftUI:更高级的 MKMapView

时间:2022-07-23
本文章向大家介绍SwiftUI:更高级的 MKMapView,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

该项目将以地图视图为基础,要求用户将要访问的地方添加到地图中。为了完成这项工作,我们不能只是在SwiftUI中嵌入一个简单的MKMapView,希望做到最好:我们需要跟踪中心坐标,用户是否在查看地点详细信息,他们拥有哪些标注等等。

因此,我们将从具有协调器的基本MKMapView包装器开始,然后在其上快速添加一些附加功能,以使其变得更加有用。

创建一个名为“ MapView”的新SwiftUI视图,为 MapKit 添加导入,然后为其提供以下代码:

struct MapView: UIViewRepresentable {
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ view: MKMapView, context: Context) {

    }

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    class Coordinator: NSObject, MKMapViewDelegate {
        var parent: MapView

        init(_ parent: MapView) {
            self.parent = parent
        }
    }
}

那里没有什么特别的,所以让我们跟踪地图的中心坐标来立即进行更改。正如我们之前所看的,这意味着在我们的协调器中实现mapViewDidChangeVisibleRegion()方法,但是这次我们将把数据传递给 MapView 结构,以便可以使用@Binding将值存储在其他位置。因此,协调器将从 MapKit 接收值并将其传递给MapView,该MapView将值放在@Binding属性中,这意味着它实际上存储在其他位置——我们将MKMapView连接到了任何嵌入了地图的 SwiftUI 视图对象。

首先将此属性添加到MapView

@Binding var centerCoordinate: CLLocationCoordinate2D

这将立即破坏MapView_Previews结构体,因为它需要提供绑定。该预览并不是真正有用的,因为MKMapView在模拟器之外无法运行,因此,如果您删除了它,我也不会怪您。但是,如果您真的想使其工作,则应向MKPointAnnotation添加一些示例数据,以便于参考:

extension MKPointAnnotation {
    static var example: MKPointAnnotation {
        let annotation = MKPointAnnotation()
        annotation.title = "London"
        annotation.subtitle = "Home to the 2012 Summer Olympics."
        annotation.coordinate = CLLocationCoordinate2D(latitude: 51.5, longitude: -0.13)
        return annotation
    }
}

将其放置在适当的位置即可轻松修复MapView_Previews,因为我们可以使用该示例注释:

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView(centerCoordinate: .constant(MKPointAnnotation.example.coordinate))
    }
}

我们将在短时间内添加更多内容,但是首先我想将其放入ContentView。在这个应用程序中,用户将要向地图上添加他们想要访问的地点,我们将通过全屏MapView和顶部的半透明圆来表示中心点来表示这些地点。尽管此视图将具有绑定来跟踪中心坐标,但是我们不需要使用该绑定来放置圆——简单的ZStack可以确保圆始终位于地图的中心。

首先,添加一条额外的导入行,以便我们可以访问MapKit的数据类型:

import MapKit

其次,在ContentView内部添加一个属性,该属性将存储地图的当前中心坐标。稍后,我们将使用它添加一个地标:

@State private var centerCoordinate = CLLocationCoordinate2D()

现在我们可以填写body属性

ZStack {
    MapView(centerCoordinate: $centerCoordinate)
        .edgesIgnoringSafeArea(.all)
    Circle()
        .fill(Color.blue)
        .opacity(0.3)
        .frame(width: 32, height: 32)
}

如果您现在运行该应用程序,您会看到可以自由移动地图,但是始终会有一个蓝色圆圈显示中心位置。

尽管我们的蓝点将始终固定在地图的中心,但是我们仍然希望ContentView在地图移动时更新其centerCoordinate属性。我们已经将其连接到MapView,但是仍然需要在地图视图的协调器中实现mapViewDidChangeVisibleRegion()方法,以启动整个链。

因此,现在将此方法添加到MapViewCoordinator类中:

func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
    parent.centerCoordinate = mapView.centerCoordinate
}

所有这些工作本身并不十分有趣,因此下一步是在右下角添加一个按钮,使我们可以在地图上添加地标。我们已经在ZStack中,因此最简单的对齐此按钮的方法是将其放置在VStackHStack中,每次放置之前都带有间隔(spacers)。这两个间隔物最终都占据了剩下的全部垂直和水平空间,从而使结尾处的所有内容都舒适地位于右下角。

我们将很快为该按钮添加一些功能,但首先让它安装到位并添加一些基本样式以使其看起来不错。

请将此VStack添加到Circle下方:

VStack {
    Spacer()
    HStack {
        Spacer()
        Button(action: {
            // 创建新的位置
        }, label: {
            Image(systemName: "plus")
        })
        .padding()
        .background(Color.black.opacity(0.75))
        .foregroundColor(.white)
        .font(.title)
        .clipShape(Circle())
        .padding(.trailing)
    }
}

PS: 此处Button 创建的闭包使用了Swift 5 修改后的尾随闭包样式,如果拷贝编译不了,请自己手动敲一下就好

请注意,我是如何在其中两次添加padding()修饰符的——一次是在添加背景色之前确保按钮更大,然后是第二次是设置整个按钮的对边缘增加一些距离。

有趣的是我们如何在地图上放置图钉。我们已将地图的中心坐标绑定到地图视图中的属性,但是现在我们需要以其他方式发送数据——我们需要在ContentView中创建位置数组,然后将其发送到MKMapView进行显示。

解决此问题的最佳方法是将问题分解为几个更小,更简单的部分。第一部分很明显:我们在ContentView中需要一个位置数组,用于存储用户要访问的所有位置。

因此,首先将此属性添加到ContentView

@State private var locations = [MKPointAnnotation]()

接下来,我们想在每当点击 + 按钮时添加一个位置。我们还不会添加标题和副标题,因此,现在这就像使用centerCoordinate的当前值创建MKPointAnnotation一样简单。

替换// 创建新的位置 注释:

let newLocation = MKPointAnnotation()
newLocation.coordinate = self.centerCoordinate
self.locations.append(newLocation)

现在是具有挑战性的部分:我们如何将其与地图视图同步?请记住,我们甚至不希望ContentView知道正在使用 MapKit,而是希望将所有功能隔离在MapView中,以便我们的SwiftUI代码保持干净。

这是updateUIView()出现的地方:当发送到UIViewRepresentable结构体的任何值发生更改时,SwiftUI都会自动调用它。然后,此方法负责将视图及其协调器与父视图中的最新配置同步。

在我们的例子中,我们将centerCoordinate绑定发送到MapView中,这意味着每当用户移动地图时,值都会更改,这又意味着始终在调用updateUIView()。由于updateUIView()为空,所以一直在悄悄发生这种情况,但是如果您在其中添加一个简单的print()调用,就会发现它栩栩如生:

func updateUIView(_ view: MKMapView, context: Context) {
    print("Updating")
}

现在,在四处移动地图时,您会一次又一次看到“Updating”打印。

无论如何,所有这些都很重要,因为我们还可以将刚才创建的locations数组传递到MapView中,并使用该数组为我们插入标注。

因此,首先将此新属性添加到MapView中,以保存我们将传递给它的所有位置:

var annotations: [MKPointAnnotation]

其次,我们需要更新 MapView_Previews,以便它发送我们的示例标注,尽管如果您已经删除了预览,我也不会怪您,因为这实际上并没有用!无论如何,如果您仍然拥有它,则将其调整为:

MapView(centerCoordinate: .constant(MKPointAnnotation.example.coordinate), annotations: [MKPointAnnotation.example])

第三,我们需要在MapView中实现updateUIView(),以便将当前标注与最新标注进行比较,如果它们不相同,则将其替换。现在,我们可以比较标注中的每个项目以查看它们是否相同,但是没有任何意义——我们不能同时添加和删除项目,因此我们要做的就是检查这两个项目是否相同数组包含相同数量的项目,如果它们不同,则删除所有现有的标注并再次添加它们。

将此替换为当前的updateUIView()方法:

func updateUIView(_ view: MKMapView, context: Context) {
    if annotations.count != view.annotations.count {
        view.removeAnnotations(view.annotations)
        view.addAnnotations(annotations)
    }
}

最后,更新ContentView,使其发送locations数组以将其转换为标注:

MapView(centerCoordinate: $centerCoordinate, annotations: locations)

到目前为止,地图工作已经足够了,因此请继续运行您的应用——您应该可以根据需要进行任意移动,然后按+按钮添加图钉。

您可能会注意到的一件事是,当iOS针紧密放置时,它们如何自动合并。例如,如果将一些图钉放在一公里的区域中然后缩小,则iOS将隐藏其中的一些图钉,以避免使地图难以阅读。

译自 Advanced MKMapView with SwiftUI