Last Updated:
Photo by henry perks on Unsplash

Integrating MapKit with SwiftUI, 續篇(1)

前文中提到我們如何利用UIViewRepresentable wrapper 將 MapKitMKMapView加入 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 裡添加程式碼。

  1. 首先,創建自定的 Coordinator class,除了繼承NSObject之外,還必須同時遵循MKMapViewDelegate protocol。(詳見文末的官方文件連結)
  2. 然後在 class 裡告訴它隸屬於誰之下(who's its parent,此例中即為 MapView)。
  3. 接著,利用makeCoordinator()來產生它的實例。
  4. 最後,則是在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
    }
    
    // ...略過程式碼
}

完成溝通橋樑的搭建之後,還需要完成三件事:

  1. 分別在 MapView 和 SwiftUI View 裡添加 property 用來存放位置座標,其中 MapView 的是 binding property,用來跟 SwiftUI View 裡的綁定,以確保資料的同步
  2. 在自定的 Coordinator 裡,完成mapViewDidChangeVisibleRegion(MKMapView)的實作。如此一來,當使用者移動地圖時,便會透過這個方法來完成要執行的動作。
  3. 最後,由於 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 裡直接觀察到其變化。


[ 補充資料 ] Apple Developer Documentation

Comments