Integrating MapKit with SwiftUI, 續篇(1)
前文中提到我們如何利用UIViewRepresentable
wrapper 將 MapKit 的MKMapView
加入 SwiftUI 的View
。雖然我們成功地為 app 加入地圖,但也僅止於此。當我們移動或扭轉地圖時,其改變的展示區域(region)及中心點(centerCoordinate)等資訊,並沒有從 SwiftUI View 傳遞給 MapView,這是因為由UIViewRepresentable
包裹而成的 MapView struct 裡缺少了負責傳遞資訊的Coordinator
。
public protocol UIViewRepresentable : View where Self.Body == Never {
...
/// Implement this method if changes to your view might affect other
/// parts of your app. In your implementation, create a custom Swift
/// instance that can communicate with other parts of your interface.
/// For example, you might provide an instance that binds its variables
/// to SwiftUI properties, causing the two to remain synchronized.
///
/// SwiftUI calls this method before calling the
/// ``UIViewRepresentable/makeUIView(context:)`` method.
func makeCoordinator() -> Self.Coordinator
}
從上方的官方說明可知,需自定一個Coordinator
,並利用makeCoordinator()
來產生其實例,此實例就可作為 MapView 和 SwiftUI View 的溝通橋樑。接著,按步驟在 MapView struct 裡添加程式碼。
- 首先,創建自定的 Coordinator class,除了繼承
NSObject
之外,還必須同時遵循MKMapViewDelegate
protocol。(詳見文末的官方文件連結) - 然後在 class 裡告訴它隸屬於誰之下(who's its parent,此例中即為 MapView)。
- 接著,利用
makeCoordinator()
來產生它的實例。 - 最後,則是在
makeUIView()
裡插入mapView.delegate = context.coordinator
。
// MapView.swift
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIView(context: Context) -> MKMapView {
let mapView = MKMapView()
// 將coordinator指派給mapView的delegate
mapView.delegate = context.coordinator
return mapView
}
// ...略過程式碼
}
完成溝通橋樑的搭建之後,還需要完成三件事:
- 分別在 MapView 和 SwiftUI View 裡添加 property 用來存放位置座標,其中 MapView 的是 binding property,用來跟 SwiftUI View 裡的綁定,以確保資料的同步。
- 在自定的 Coordinator 裡,完成
mapViewDidChangeVisibleRegion(MKMapView)
的實作。如此一來,當使用者移動地圖時,便會透過這個方法來完成要執行的動作。 - 最後,由於 MapView 多了新的
centerCoordinate
property,別忘了在 SwiftUI View 裡的MapView()
引入參數,並記得加上 binding 的$
符號。
// MapView.swift
import SwiftUI
import MapKit
struct MapView: UIViewRepresentable {
@Binding var centerCoordinate: CLLocationCoordinate2D
class Coordinator: NSObject, MKMapViewDelegate {
var parent: MapView
init(_ parent: MapView) {
self.parent = parent
}
// 當使用者移動地圖時,Coordinator會自動呼叫此方法完成所需執行的動作
func mapViewDidChangeVisibleRegion(_ mapView: MKMapView) {
parent.centerCoordinate = mapView.centerCoordinate
}
}
// ...略過程式碼
}
// ContentView.swift
// ...略過程式碼
struct ContentView: View {
@State private var centerCoordinate = CLLocationCoordinate2D()
var body: some View {
ZStack {
MapView(centerCoordinate: $centerCoordinate)
.edgesIgnoringSafeArea(.all)
}
}
}
完成以上步驟之後,當我們再次拖拉或扭轉地圖時,SwiftUI View 便會將變動的狀態透過 Coordinator 傳遞給 MapView,各位不妨在mapViewDidChangeVisibleRegion()
和updateUIView()
裡分別加上print("center at: \(parent.centerCoordinate)")
和print("center at: \(centerCoordinate)")
,這樣就能在 Xcode 裡直接觀察到其變化。
Comments