精通 SwiftUI - iOS 17 版

第 46 章
使用地圖和標註

MapKit 是一個強大的框架,允許開發者將地圖、標註和基於位置的功能添加到他們的 iOS 應用中。在 SwiftUI 中,你可以輕鬆地將 MapKit 集成到您的應用中,創建交互式和動態的地圖,提供出色的用戶體驗。在本教學中,我們將探索如何在 SwiftUI 中使用地圖和標註,以及如何自定義地圖的樣式和相機位置。

MapKit 基礎知識

讓我們從 MapKit 的基礎知識開始。MapKit 框架包括一個 Map 視圖,開發者可以在任何 SwiftUI 項目中使用它來嵌入一個地圖。以下是一個示例:

import SwiftUI
import MapKit

struct ContentView: View {
  var body: some View {
    Map()
  }
}

在使用 Map 視圖之前,你需要導入 MapKit 框架。然後,要創建一個地圖,只需實例化一個 Map 視圖。如果你在 Xcode 中打開了預覽畫布,你應該在模擬器中看到一個全螢幕的地圖。

圖 46.1. 基本 Map 視圖
圖 46.1. 基本 Map 視圖

使用地圖相機更改初始位置

除了顯示預設位置外,Map 視圖還有另一個 init 方法,讓你可以更改地圖的初始位置:

init(
    initialPosition: MapCameraPosition,
    bounds: MapCameraBounds? = nil,
    interactionModes: MapInteractionModes = .all,
    scope: Namespace.ID? = nil
) where Content == MapContentView<Never, EmptyMapContent>

你可以使用 MapCameraPosition 的實例作為地圖的初始位置。MapCameraPosition 包含多個屬性,你可以使用這些屬性來控制顯示哪個地點或區域,包括:

  • automatic
  • item(MKMapItem) - 用於顯示特定的地圖項目。
  • region(MKCoordinateRegion) - 用於顯示特定的區域。
  • rect(MKMapRect) - 用於顯示特定的地圖邊界。
  • camera(MapCamera) - 用於顯示現有的相機位置。
  • userLocation() - 用於顯示使用者的位置。

例如,你可以使用 .region(MKCoordinateRegion) 指示地圖顯示特定的區域:

Map(initialPosition: .region(MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: 40.75773, longitude: -73.985708), span: MKCoordinateSpan(latitudeDelta: 0.05, longitudeDelta: 0.05))))

上面示例中的坐標是紐約時代廣場的 GPS 坐標。span 的值用於定義地圖的縮放級別。值越小,縮放級別越高。

圖 46.2. 更改地圖視圖的初始位置
圖 46.2. 更改地圖視圖的初始位置

如果你要顯示特定的位置,可以傳遞地圖項目作為初始位置。以下是一個程式碼示例:

extension CLLocationCoordinate2D {
    static let bigBen = CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.124570)
}

struct ContentView: View {

    var body: some View {
        Map(initialPosition: .item(MKMapItem(placemark: .init(coordinate: .bigBen))))
    }
}

動畫化變更地圖位置

Map視圖還提供了一個額外的init方法,接受一個對MapCameraPosition的綁定。如果你需要更改地圖的位置,這個init` 方法更為適用:

@State private var position: MapCameraPosition = .automatic

Map(position: $position) {
   .
   .
   .
}

例如,如果你想要添加兩個按鈕,供使用者在兩個位置之間切換,可以編寫以下程式碼:

extension CLLocationCoordinate2D {
    static let bigBen = CLLocationCoordinate2D(latitude: 51.500685, longitude: -0.124570)
    static let towerBridge = CLLocationCoordinate2D(latitude: 51.505507, longitude: -0.075402)
}

struct ContentView: View {

    @State private var position: MapCameraPosition = .automatic

    var body: some View {
        Map(position: $position)
            .onAppear {
                position = .item(MKMapItem(placemark: .init(coordinate: .bigBen)))
            }
            .safeAreaInset(edge: .bottom) {
                HStack {
                    Button(action: {
                        withAnimation {
                            position = .item(MKMapItem(placemark: .init(coordinate: .bigBen)))
                        }
                    }) {
                        Text("Big Ben")
                    }
                    .tint(.black)
                    .buttonStyle(.borderedProminent)

                    Button(action: {
                        withAnimation {
                            position = .item(MKMapItem(placemark: .init(coordinate: .towerBridge)))
                        }
                    }) {
                        Text("Tower Bridge")
                    }
                    .tint(.black)
                    .buttonStyle(.borderedProminent)
                }
            }    
    }
}

通過將 position 變量包裹在 withAnimation 中,地圖視圖將自動進行位置變化的動畫。

圖 46.3. 動態轉換位置
圖 46.3. 動態轉換位置

當你提供帶有傾斜角度的 MapCamera 以創建3D透視效果時,此動畫效果會更好。要查看效果,可以嘗試更改以下程式碼中的 Big Ben 位置:

position = .camera(MapCamera(
                                        centerCoordinate: .bigBen, 
                                        distance: 800, 
                                        heading: 90, 
                                        pitch: 50))

當你預覽地圖視圖時,相機角度會相應調整以顯示該區域的3D透視效果。

圖 46.4. 使用3D透視效果
圖 46.4. 使用3D透視效果

添加標記和註釋

圖 46.5. 地圖標記
圖 46.5. 地圖標記

標記是 MapKit 中的一個有用功能,它允許你在地圖的特定座標上顯示內容。它為你的地圖添加了額外的信息層,例如商店或餐廳。標記可以使用系統圖像和色調顏色進行自定義,使它們在視覺上與眾不同,易於識別。無論你是在構建導航 App 還是旅遊指南,標記都是一個有價值的工具,可以幫助你創建更好的使用者體驗。

要添加一個標記,你可以在地圖內容生成器閉包中創建 Marker 視圖,如下所示:

Map(position: $position) {
    Marker("Pickup here", coordinate: .pickupLocation)
}

你還可以選擇性地使用系統圖像自定義 Marker 對象。要更改標記的顏色,請使用 tint 修飾器:

Marker("Pickup here", 
                systemImage: "car.front.waves.up", 
                coordinate: .pickupLocation)
                .tint(.purple)

除了 Marker,SwiftUI 在 iOS 17 中還引入了一個名為 Annotation 的視圖,用於在地圖上標示位置。它的功能與 Marker 類似,但提供了更大的自定義彈性。

要添加一個註釋,你可以在地圖內容閉包中創建一個 Annotation 視圖。以下是添加一個簡單註釋的程式碼示例:

Map(position: $position) {
    Annotation("Pick up", coordinate: .pickupLocation, anchor: .bottom) {
        Image(systemName: "car.front.waves.up")
    }
}

你可以以多種方式自定義註釋。通過對其應用不同的修飾器,你可以改變其外觀和行為。此外,還可以使用堆疊視圖來排列註釋的不同組件,以創建符合所需求的佈局。以下是一個示例:

Annotation("Pick up", coordinate: .pickupLocation, anchor: .bottom) {
    ZStack {
        Circle()
            .foregroundStyle(.indigo.opacity(0.5))
            .frame(width: 80, height: 80)

        Image(systemName: "car.front.waves.up")
            .symbolEffect(.variableColor)
            .padding()
            .foregroundStyle(.white)
            .background(Color.indigo)
            .clipShape(Circle())
    }
}

這將產生一個如下圖所示的動畫註釋效果。

圖 46.6. 自定義標記
圖 46.6. 自定義標記

更改地圖樣式

在預設情況下,地圖視圖以標准樣式呈現地圖。但是,你可以使用 mapStyle 修飾器來更改樣式:

Map {

}
.mapStyle(.imagery(elevation: .realistic))

這將創建一個基於衛星影像的地圖樣式。通過指定 realistic 的標高(elevation),地圖視圖將呈現出具有逼真外觀的3D地圖。

圖 46.7. 逼真外觀的3D地圖
圖 46.7. 逼真外觀的3D地圖

你還可以選擇將地圖樣式更改為混合模式,如下所示:

.mapStyle(.hybrid)

感興趣的地點(Points of Interest)

如果你想要突出顯示某個位置的興趣點,可以使用 MKLocalSearch 對象創建一個搜索請求,並使用 Marker 來顯示它們。將以下函數插入到 ContentView 中:

private func search(location: CLLocationCoordinate2D, query: String) {
    let request = MKLocalSearch.Request()
    request.naturalLanguageQuery = query
    request.resultTypes = .pointOfInterest
    request.region = MKCoordinateRegion(
                        center: location,
                        latitudinalMeters: 100,
                        longitudinalMeters: 100)

    Task {
        let search = MKLocalSearch(request: request)
        let response = try? await search.start()
        searchResults = response?.mapItems ?? []
    }
}

這個 search 函數接受一個位置變數和一個字符串查詢作為參數。例如,如果你想要搜索 Tower Bridge 周圍的餐廳,可以像這樣寫:

search(location: .towerBridge, query: "restaurants")

我們使用 MKLocalSearch 生成一個以 "restaurants" 為查詢的搜索請求,並將結果類型指定為 .pointOfInterestregion 屬性為搜索提供了一個位置提示。返回的結果是一個 MKMapItem 數組,我們將其存儲在一個尚未聲明的狀態變量中。因此,請在 ContentView 的開頭插入以下程式碼:

@State private var searchResults: [MKMapItem] = []

要啟動搜索,請將以下程式碼插入到 Big Ben 按鈕的動作閉包中:

search(location: .bigBen, query: "restaurants")

而且,在 Tower Bridge 按鈕的動作閉包中插入另一行程式碼:

search(location: .towerBridge, query: "restaurants")

當使用者點擊其中任一個按鈕來切換位置時,App還將搜索並顯示該位置周圍的興趣點(POI)。

最後,為了在地圖上標出這些 POI,我們將使用 Marker 來映射這些位置。請在 Map 閉包中插入以下程式碼:

ForEach(searchResults, id: \.self) { result in
    Marker(item: result)
}

完成必要的更改後,你可以在預覽視圖中測試 App。該 App 會根據你點擊的按鈕顯示位於 Tower Bridge 或 Big Ben 周圍的餐廳。

圖 46.8. 顯示興趣點
圖 46.8. 顯示興趣點

總結

本教學介紹了如何在 SwiftUI 中使用 MapKit 框架來處理地圖和註釋。最新版本的 SwiftUI 為開發人員提供了額外的 API 和視圖,以進一步自定義地圖視圖。到目前為止,你應該已經知道如何在應用程序中嵌入地圖並添加註釋以突出顯示地圖上的位置。

供參考,你可以在此處下載演示項目: