使用 SwiftUI 在地图中绘制坐标轨迹

4 minute read

一直有想做属于自己的 App 的想法,零零散散地有学习一些内容,现在决定开启一个记录学习过程的系列博文,以此来巩固自己所学到的 iOS 开发知识和激励自己把第一个 App 实现出来。 脑海中一直有个做地图类应用的想法,本科做移动轨迹相关的毕业设计。此类应用主要包括地图(Map)、坐标点(Location)、绘制轨迹(Polyline)几个要素。这篇博文将会使用 SwiftUI 在地图上连接坐标点绘制出轨迹。GPS 坐标数据可以在 microsoft 上下载。效果如下: 首先创建一个 SwiftUI 工程的项目,项目的文件组织如下: 先定义坐标点的数据结构,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import Foundation

struct Location: Hashable, Codable, Identifiable {
    var id: Int
    // 经度
    var longitude: Double
    // 纬度
    var latitude: Double
    var isValid: Bool
}

坐标数据的 ViewModel,我们将从 location.json 中加载所有的坐标点,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Foundation

final class LocationViewModel: ObservableObject {
    
    @Published var locations: [Location] = load("location.json")
    
}

func load<T: Decodable>(_ fileName: String) -> T {
    // The data value type allows simple byte buffers to take on behavior of Foundation objects.
    let data: Data
    
    guard let file = Bundle.main.url(forResource: fileName, withExtension: nil) else {
        fatalError("Couldn't find \(fileName) in main bundle.")
    }
    
    do {
        data = try Data(contentsOf: file)
    } catch {
        fatalError("Couldn't load \(fileName) from main bundle:\n\(error)")
    }
    
    do {
        let decoder = JSONDecoder()
        return try decoder.decode(T.self, from: data)
    } catch {
        fatalError("Couldn't parse \(fileName) as \(T.self):\n\(error)")
    }
}

接下来我们将利用 UIViewRepresentable 这个协议实现将 UIKit 的 UIView 集成到 SwiftUI 中使用。UIViewRepresentable 只包含四个方法。其中 makeUIView , updateUIView 用于创建和更新视图。makeCoordinator 创建协调器,用于通知其它 UI 与该 View 之间的变化更新。dismantleUIView 用于在移除 View 时做一些善后操作。四个方法在 UIViewRepresentable 生命周期中的调用顺序如下: 我们的 MapView 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import SwiftUI
import MapKit

struct MapView: UIViewRepresentable {
    
    private let locationViewModel = LocationViewModel()
    private let mapZoomEdgeInsets = UIEdgeInsets(top: 30.0, left: 30.0, bottom: 30.0, right: 30.0)
      
    
    func makeCoordinator() -> MapViewCoordinator {
        MapViewCoordinator(self)
    }
    
    func makeUIView(context: Context) -> MKMapView {
        let mapView = MKMapView()
        mapView.showsUserLocation = true
        mapView.delegate = context.coordinator
        return mapView
    }

    func updateUIView(_ uiView: MKMapView, context: UIViewRepresentableContext<MapView>) {
        updateOverlays(from: uiView)
    }
    
    private func updateOverlays(from mapView: MKMapView) {
        mapView.removeOverlays(mapView.overlays)
        
        var coordinates = [CLLocationCoordinate2D]()
        for i in 0..<locationViewModel.locations.count {
            let location = locationViewModel.locations[i]
            coordinates.append(CLLocationCoordinate2D(latitude: location.latitude, longitude: location.longitude))
        }
        
        let polyline = MKPolyline(coordinates: coordinates, count: locationViewModel.locations.count)
        mapView.addOverlay(polyline)
        setMapZoomArea(map: mapView, polyline: polyline, edgeInsets: mapZoomEdgeInsets, animated: true)
    }
      
    private func setMapZoomArea(map: MKMapView, polyline: MKPolyline, edgeInsets: UIEdgeInsets, animated: Bool = false) {
        map.setVisibleMapRect(polyline.boundingMapRect, edgePadding: edgeInsets, animated: animated)
    }
}

struct MapView_Previews: PreviewProvider {
    static var previews: some View {
        MapView()
    }
}

协调器 MayViewCoordinator 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import Foundation
import MapKit

final class MapViewCoordinator: NSObject, MKMapViewDelegate {
    
    private let mapView: MapView
    
    init(_ control: MapView) {
        self.mapView = control
    }
    
    func mapView(_ mapView: MKMapView, didAdd views: [MKAnnotationView]) {
        if let annotationView = views.first, let annotation = annotationView.annotation {
            if annotation is MKUserLocation {
                let region = MKCoordinateRegion(center: annotation.coordinate, latitudinalMeters: 1000, longitudinalMeters: 1000)
                mapView.setRegion(region, animated: true)
            }
        }
    }

    func mapView(_ mapView: MKMapView, rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
        let renderer = MKPolylineRenderer(overlay: overlay)
        // set the line properties such as color and width
        renderer.strokeColor = .blue
        renderer.lineWidth = 3.0
        return renderer
    }
}

参考链接 https://www.fatbobman.com/posts/uikitInSwiftUI/ https://medium.com/@mauvazquez/decoding-a-polyline-and-drawing-it-with-swiftui-mapkit-611952bd0ecb