1. 事前準備
本程式碼研究室將教導您如何搭配 Mapsif for iOS 使用 SwiftUI。
必要條件
- Swift 基本知識
- 對 SwiftUI 的基本概念
執行步驟
- 啟用並使用 Maps SDK for iOS,使用 SwiftUI 將 Google 地圖新增至 iOS 應用程式。
- 在地圖上加入標記。
- 將 SwiftUI 檢視的狀態傳遞至
GMSMapView
物件,反之亦然。
軟硬體需求
- Xcode 11.0 以上版本
- 已啟用帳單功能的 Google 帳戶
- Maps SDK for iOS
- 車庫
2. 做好準備
在下列啟用步驟中,啟用 Maps SDK for iOS。
設定 Google 地圖平台
如果您還沒有 Google Cloud Platform 帳戶和已啟用計費功能的專案,請參閱開始使用 Google 地圖平台指南,建立帳單帳戶和專案。
- 在 Cloud Console 中按一下專案下拉式選單,然後選取您要用於這個程式碼研究室的專案。
3. 下載範例程式碼
以下提供一些入門程式碼,協助您快速上手,幫助您快速上手。我們決定直接跳到解決方案,但如果您想依照自己的所有步驟逐步進行,請繼續閱讀本文。
- 如果您已安裝
git
,請複製存放區。
git clone https://github.com/googlecodelabs/maps-ios-swiftui.git
或者,您也可以點擊下方按鈕來下載原始碼。
- 收到驗證碼後,在終端機
cd
進入starter/GoogleMapsSwiftUI
字典。 - 執行
carthage update --platform iOS
即可下載 Maps SDK for iOS - 最後,請在 Xcode 中開啟
GoogleMapsSwiftUI.xcodeproj
檔案
4. 程式碼總覽
在您下載的入門專案中,我們為您提供了並實作下列類別:
AppDelegate
- 應用程式的UIApplicationDelegate
。而 Maps SDK for iOS 將初始化。City
- 代表城市的結構 (包含城市的名稱和座標)。MapViewController
- 包含 UI 的簡單 UIKitUIViewController
(GMSMapView)SceneDelegate
- 用於執行ContentView
的應用程式UIWindowSceneDelegate
。
此外,下列類別具有部分導入,將由您透過本程式碼研究室結束:
ContentView
:包含應用程式的頂層 SwiftUI 檢視。MapViewControllerBridge
:這個類別可將 UIKit 檢視連結到 SwiftUI 檢視。具體而言,這門課程能讓MapViewController
在 SwiftUI 中存取。
5. 使用 SwiftUI 與 UIKit 的比較
SwiftUI 是在 iOS 13 中推出,用來替代 iOS 應用程式的開發使用者介面,而非 UIKit。與前一版的 UIKit 相比,SwiftUI 有許多優點。如何命名幾個項目:
- 狀態會隨著狀態改變而自動更新。使用稱為 State 的物件,只要物件所含的值有所變更,使用者介面就會自動更新。
- 即時預覽可以加快開發速度。即時預覽功能可以降低建構與部署程式碼到模擬器的作業流程,因為 Xcode 可以清楚顯示 SwiftUI 視圖的預覽畫面。
- 資料來源為 Swift。系統會在 Swift 中宣告所有 SwiftUI 檢視,因此不再需要使用介面製作工具。
- 與 UIKit 互通。與 UIKit 互通,可確保現有應用程式能夠逐步使用 SwiftUI 和現有資料檢視。此外,尚未支援 SwiftUI 的程式庫 (例如 Maps SDK for iOS) 仍可在 SwiftUI 中使用。
部分缺點:
- SwiftUI 僅適用於 iOS 13 以上版本。
- 無法在 Xcode 預覽中檢查檢視階層。
SwiftUI 狀態和資料流程
SwiftUI 提供使用宣告式方法建立 UI 的全新方式,您可以告訴 SwiftUI 您希望檢視畫面的外觀及其他不同狀態,然後由系統代勞。SwiftUI 會根據事件或使用者動作,在基礎狀態變更時處理更新。這個設計通常稱為「單向資料流程」。雖然此程式碼的細節不在這個程式碼研究室的範圍內,但仍建議您閱讀有關 Apple 的狀態與資料流程說明文件,瞭解這項功能的運作方式。
使用 UIViewRepresentable 或 UIViewControllerRepresentable 處理 UIKit 和 SwiftUI
由於 Maps SDK for iOS 是以 UIKit 為基礎建立而成,因此尚未提供與 SwiftUI 相容的檢視選項,因此如要使用 SwiftUI,就必須符合 UIViewRepresentable
或 UIViewControllerRepresentable
的要求。這些通訊協定可讓 SwiftUI 分別加入 UIKit 建構的 UIView
和 UIViewController
。雖然您可以使用任一通訊協定將 Google 地圖加入 SwiftUI 檢視,但在下一個步驟中,我們來看看如何使用 UIViewControllerRepresentable
來包含包含地圖的 UIViewController
。
6. 新增地圖
在本節中,您會將 Google 地圖新增到 SwiftUI 檢視。
新增 API 金鑰
您在先前步驟中建立的 API 金鑰必須提供給 Maps SDK for iOS,才能將您的帳戶與應用程式中顯示的地圖建立關聯。
如要提供 API 金鑰,請開啟 AppDelegate.swift
檔案並前往 application(_, didFinishLaunchingWithOptions)
方法。目前,SDK 是透過 GMSServices.provideAPIKey()
初始化,字串為「YOUR_API_KEY」。然後將該字串替換成您的 API 金鑰。完成此步驟後,應用程式啟動時,Maps SDK for iOS 將初始化。
使用 MapViewControllerBridge 新增 Google 地圖
現在您的 API 金鑰已提供給 SDK,接下來要在應用程式中顯示地圖。
在範例程式碼中提供的檢視控制項「MapViewController
」目前含有一個 GMSMapView
。不過,由於這個檢視控制項是在 UIKit 中建立,所以您必須將這個類別連結到 SwiftUI,才能在 ContentView
中使用。方法如下:
- 在 Xcode 中開啟「
MapViewControllerBridge
」檔案。
此類別符合 UIViewControllerRepresentable,這是將 UIKit UIViewController
包裝所需的通訊協定,以便做為 SwiftUI 檢視使用。換句話說,只要遵循此通訊協定,您就能將 UIKit 檢視連結到 SwiftUI 檢視。符合此通訊協定需要實作兩種方法:
makeUIViewController(context)
- SwiftUI 會呼叫這個方法來建立基礎UIViewController
。您可以在這裡為UIViewController
執行個體化,並傳送其初始狀態。updateUIViewController(_, context)
- SwiftUI 會在狀態改變時呼叫這個方法。您可以在這裡修改基礎UIViewController
,以因應狀態變更。
- 建立
MapViewController
在 makeUIViewController(context)
函式中,將新的 MapViewController
執行個體化並傳回結果。完成上述步驟後,您的 MapViewControllerBridge
應該會如下所示:
MapViewControllerBridge
import GoogleMaps
import SwiftUI
struct MapViewControllerBridge: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> MapViewController {
return MapViewController()
}
func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
}
}
在 ContentView 中使用 MapViewControllerBridge
MapViewControllerBridge
現在建立的是 MapViewController
的例項,下一步則是在 ContentView
中使用此結構以顯示地圖。
- 在 Xcode 中開啟「
ContentView
」檔案。
ContentView
已在 SceneDelegate
中執行個體化,其中包含頂層應用程式檢視。系統將在這個檔案中新增地圖。
- 在
body
屬性中建立MapViewControllerBridge
。
在這個檔案的 body
屬性中,系統已為您提供了並實作了 ZStack
。ZStack
目前包含可互動和拖曳的城市清單,您可以在後續步驟中使用。目前,在 ZStack
內建立 MapViewControllerBridge
時,會成為 ZStack
的第一個子檢視,因此地圖會隨即顯示在城市檢視清單後方的應用程式。執行此動作時,ContentView
中 body
屬性的內容應如下所示:
內容檢視
var body: some View {
let scrollViewHeight: CGFloat = 80
GeometryReader { geometry in
ZStack(alignment: .top) {
// Map
MapViewControllerBridge()
// Cities List
CitiesList(markers: $markers) { (marker) in
guard self.selectedMarker != marker else { return }
self.selectedMarker = marker
self.zoomInCenter = false
self.expandList = false
} handleAction: {
self.expandList.toggle()
} // ...
}
}
}
- 現在,請直接執行應用程式吧。現在您的裝置畫面上應該將地圖載入量和可拖曳的城市清單朝向螢幕底部顯示。
7. 在地圖上加入標記
在上一個步驟中,您加入了一個可互動清單的地圖,上面有一份城市清單。在本節中,您將為清單中的每個城市新增標記。
標記狀態
ContentView
目前正在宣告名為 markers
的屬性,此清單為 GMSMarker
,代表 cities
靜態屬性中宣告的每個城市。請注意,此屬性會以 SwiftUI 屬性包裝函式 State 加註,表示該屬性應由 SwiftUI 管理。因此,如果系統偵測到任何與此屬性有關的任何變更 (例如新增或移除標記),就會更新使用該狀態的檢視。
內容檢視
static let cities = [
City(name: "San Francisco", coordinate: CLLocationCoordinate2D(latitude: 37.7576, longitude: -122.4194)),
City(name: "Seattle", coordinate: CLLocationCoordinate2D(latitude: 47.6131742, longitude: -122.4824903)),
City(name: "Singapore", coordinate: CLLocationCoordinate2D(latitude: 1.3440852, longitude: 103.6836164)),
City(name: "Sydney", coordinate: CLLocationCoordinate2D(latitude: -33.8473552, longitude: 150.6511076)),
City(name: "Tokyo", coordinate: CLLocationCoordinate2D(latitude: 35.6684411, longitude: 139.6004407))
]
/// State for markers displayed on the map for each city in `cities`
@State var markers: [GMSMarker] = cities.map {
let marker = GMSMarker(position: $0.coordinate)
marker.title = $0.name
return marker
}
請注意,ContentView
會使用 markers
屬性將城市傳遞至 CitiesList
類別,以顯示城市清單。
城市清單
struct CitiesList: View {
@Binding var markers: [GMSMarker]
var body: some View {
GeometryReader { geometry in
VStack(spacing: 0) {
// ...
// List of Cities
List {
ForEach(0..<self.markers.count) { id in
let marker = self.markers[id]
Button(action: {
buttonAction(marker)
}) {
Text(marker.title ?? "")
}
}
}.frame(maxWidth: .infinity)
}
}
}
}
透過 繫結,將狀態傳送到 MapViewControllerBridge
除了顯示 markers
屬性資料的城市清單以外,請將這個屬性傳送至 MapViewControllerBridge
結構,讓該物件能用來在地圖上顯示這些標記。請按照下列步驟操作:
- 在
MapViewControllerBridge
中宣告具有@Binding
註解的新markers
屬性
MapViewControllerBridge
struct MapViewControllerBridge: : UIViewControllerRepresentable {
@Binding var markers: [GMSMarker]
// ...
}
- 在
MapViewControllerBridge
中,更新updateUIViewController(_, context)
方法以使用markers
屬性
如先前步驟所述,SwiftUI 會在狀態變更時呼叫 updateUIViewController(_, context)
。這個方法是我們會更新地圖,因此要在 markers
中顯示標記。如要這麼做,請先更新每個標記的 map
屬性。完成這個步驟後,您的 MapViewControllerBridge
看起來應該會像這樣:
import GoogleMaps
import SwiftUI
struct MapViewControllerBridge: UIViewControllerRepresentable {
@Binding var markers: [GMSMarker]
func makeUIViewController(context: Context) -> MapViewController {
return MapViewController()
}
func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
// Update the map for each marker
markers.forEach { $0.map = uiViewController.map }
}
}
- 將
markers
屬性從ContentView
傳送到MapViewControllerBridge
由於您在 MapViewControllerBridge
中新增了屬性,現在此屬性的值必須透過 MapViewControllerBridge
的初始化工具傳遞。因此,如果您嘗試建構應用程式,請注意應用程式無法編譯。如要解決這個問題,請將 ContentView
更新為建立 MapViewControllerBridge
的地方,並傳遞 markers
屬性,如下所示:
struct ContentView: View {
// ...
var body: some View {
// ...
GeometryReader { geometry in
ZStack(alignment: .top) {
// Map
MapViewControllerBridge(markers: $markers)
// ...
}
}
}
}
請注意,前置字串 $
用來在 markers
中傳遞至 MapViewControllerBridge
,因為這是預期屬性。$
是與 Swift 屬性包裝函式搭配使用的保留前置字元。套用至某個州時,系統會傳回繫結。
- 請直接執行應用程式,即可在地圖上顯示標記。
8. 為所選城市建立動畫
在上一個步驟中,您會將狀態從某個 SwiftUI 檢視傳送至另一個,以在地圖中加入標記。在這個步驟中,您將需要在可互動清單中輕觸某個城市/標記。如要執行動畫,當變更時,您必須修改地圖的相機位置,藉此回應狀態。如要進一步瞭解地圖相機的概念,請參閱相機和檢視畫面。
為選取的城市建立動畫地圖
如何將地圖設為所選城市:
- 在
MapViewControllerBridge
中定義新繫結
ContentView
具有名為 selectedMarker
的 State 屬性,它將初始化為「nil」,並在清單中選取城市時更新。這會由 ContentView
中的 CitiesList
資料檢視 buttonAction
處理。
內容檢視
CitiesList(markers: $markers) { (marker) in
guard self.selectedMarker != marker else { return }
self.selectedMarker = marker
// ...
}
當 selectedMarker
變更時,MapViewControllerBridge
應瞭解此狀態變更,以讓地圖為選取的標記製作動畫。因此,在 GMSMarker
的 MapViewControllerBridge
中定義新繫結並命名為 selectedMarker
。
MapViewControllerBridge
struct MapViewControllerBridge: UIViewControllerRepresentable {
@Binding var selectedMarker: GMSMarker?
}
- 更新
MapViewControllerBridge
,讓地圖在selectedMarker
出現變化時建立動畫
宣告新的繫結後,您必須更新 MapViewControllerBridge
的 updateUIViewController_, context)
函式,將地圖轉成選取的標記。請直接複製下方的程式碼,即可開始使用:
struct MapViewControllerBridge: UIViewControllerRepresentable {
@Binding var selectedMarker: GMSMarker?
func updateUIViewController(_ uiViewController: MapViewController, context: Context) {
markers.forEach { $0.map = uiViewController.map }
selectedMarker?.map = uiViewController.map
animateToSelectedMarker(viewController: uiViewController)
}
private func animateToSelectedMarker(viewController: MapViewController) {
guard let selectedMarker = selectedMarker else {
return
}
let map = viewController.map
if map.selectedMarker != selectedMarker {
map.selectedMarker = selectedMarker
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
map.animate(toZoom: kGMSMinZoomLevel)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
map.animate(with: GMSCameraUpdate.setTarget(selectedMarker.position))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
map.animate(toZoom: 12)
})
}
}
}
}
}
animateToSelectedMarker(viewController)
函式會使用 GMSMapView
's 的 animate(with)
函式執行一系列地圖動畫。
- 將
ContentView
的selectedMarker
傳送到MapViewControllerBridge
MapViewControllerBridge
宣告新的繫結後,請更新 ContentView
以傳遞 selectedMarker
,讓 MapViewControllerBridge
執行個體化。
內容檢視
struct ContentView: View {
// ...
var body: some View {
// ...
GeometryReader { geometry in
ZStack(alignment: .top) {
// Map
MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker)
// ...
}
}
}
}
完成此步驟後,每當您在清單中選取新的城市時,地圖就會為該動畫製作動畫。
動畫 SwiftUI 以欣賞城市美景
SwiftUI 可輕鬆處理「檢視狀態」動畫的動畫,因此檢視檢視畫面變得簡單。為了示範這一點,您將在地圖動畫完成之後,將檢視畫面移至所選城市,以新增更多動畫。為達成這個目標,請完成下列步驟:
- 將「
onAnimationEnded
」設為MapViewControllerBridge
由於 SwiftUI 動畫將在您先前新增的地圖動畫之後執行,請在 MapViewControllerBridge
中宣告名為 onAnimationEnded
的新關閉,並在 animateToSelectedMarker(viewController)
方法的最後一個地圖動畫延遲 0.5 秒後叫用這個關閉。
MapViewControllerBridge
struct MapViewControllerBridge: UIViewControllerRepresentable {
var onAnimationEnded: () -> ()
private func animateToSelectedMarker(viewController: MapViewController) {
guard let selectedMarker = selectedMarker else {
return
}
let map = viewController.map
if map.selectedMarker != selectedMarker {
map.selectedMarker = selectedMarker
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
map.animate(toZoom: kGMSMinZoomLevel)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
map.animate(with: GMSCameraUpdate.setTarget(selectedMarker.position))
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
map.animate(toZoom: 12)
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: {
// Invoke onAnimationEnded() once the animation sequence completes
onAnimationEnded()
})
})
}
}
}
}
}
- 在「
MapViewControllerBridge
」中導入onAnimationEnded
實作 onAnimationEnded
關閉,其中 MapViewControllerBridge
會在 ContentView
中執行個體化。複製並貼上以下程式碼,以新增名為 zoomInCenter
的新狀態,並利用 clipShape
來修改檢視區塊,並依據 zoomInCenter
的值調整裁剪形狀的直徑
內容檢視
struct ContentView: View {
@State var zoomInCenter: Bool = false
// ...
var body: some View {
// ...
GeometryReader { geometry in
ZStack(alignment: .top) {
// Map
let diameter = zoomInCenter ? geometry.size.width : (geometry.size.height * 2)
MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker, onAnimationEnded: {
self.zoomInCenter = true
})
.clipShape(
Circle()
.size(
width: diameter,
height: diameter
)
.offset(
CGPoint(
x: (geometry.size.width - diameter) / 2,
y: (geometry.size.height - diameter) / 2
)
)
)
.animation(.easeIn)
.background(Color(red: 254.0/255.0, green: 1, blue: 220.0/255.0))
}
}
}
}
- 現在就開始執行應用程式吧!
9. 傳送事件至 SwiftUI
在這個步驟中,您會監聽 GMSMapView
傳送的事件,然後將該事件傳送至 SwiftUI。具體而言,您將在「地圖檢視」中設定委派對象,並監聽「相機」移動事件,如此一來,當該城市以焦點為中心,而地圖相機從手勢移動後,地圖檢視就會聚焦,讓您查看更多地圖內容。
使用 SwiftUI 協調器
GMSMapView
會發出事件,例如攝影機位置變更或使用者輕觸標記時。監聽這些事件的機制是透過 GMSMapViewDelegate 通訊協定。SwiftUI 推出「Coordinator」的概念,這個概念專門用於 UIKit 檢視控制器的委派對象。因此,在 SwiftUI 世界中,協調人員應負責遵守 GMSMapViewDelegate
通訊協定。如要這樣做,請按照下列步驟進行:
- 在
MapViewControllerBridge
中建立名稱為MapViewCoordinator
的協調員
在 MapViewControllerBridge
類別中建立巢狀類別並呼叫 MapViewCoordinator
。此類別應符合 GMSMapViewDelegate
,且應宣告 MapViewControllerBridge
為屬性。
MapViewControllerBridge
struct MapViewControllerBridge: UIViewControllerRepresentable {
// ...
final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
var mapViewControllerBridge: MapViewControllerBridge
init(_ mapViewControllerBridge: MapViewControllerBridge) {
self.mapViewControllerBridge = mapViewControllerBridge
}
}
}
- 在「
MapViewControllerBridge
」中導入makeCoordinator()
接著,在 MapViewControllerBridge
中實作 makeCoordinator()
方法,並傳回您在前一步驟建立的 MapViewCoodinator
例項。
MapViewControllerBridge
struct MapViewControllerBridge: UIViewControllerRepresentable {
// ...
func makeCoordinator() -> MapViewCoordinator {
return MapViewCoordinator(self)
}
}
- 將
MapViewCoordinator
設為地圖檢視
建立自訂協調人員後,下一步是將協調者設為檢視控制項地圖檢視的委任對象。如要這麼做,請在 makeUIViewController(context)
中更新檢視控制器初始化功能。您在上一個步驟中建立的協調器可透過 Context 物件存取。
MapViewControllerBridge
struct MapViewControllerBridge: UIViewControllerRepresentable {
// ...
func makeUIViewController(context: Context) -> MapViewController {
let uiViewController = MapViewController()
uiViewController.map.delegate = context.coordinator
return uiViewController
}
- 在「
MapViewControllerBridge
」中新增封閉式鏡頭後,攝影機就會移動活動
由於目標是要透過相機移動更新視圖,因此請宣告新的關閉屬性 (接受在 MapViewControllerBridge
中為「mapViewWillMove
」的布林值),並在 MapViewCoordinator
的委派方法「mapView(_, willMove)
」中叫用這個關閉方式。將 gesture
的值傳送到上鎖,這樣 SwiftUI 視圖只能對手勢相關的相機移動事件做出反應。
MapViewControllerBridge
struct MapViewControllerBridge: UIViewControllerRepresentable {
var mapViewWillMove: (Bool) -> ()
//...
final class MapViewCoordinator: NSObject, GMSMapViewDelegate {
// ...
func mapView(_ mapView: GMSMapView, willMove gesture: Bool) {
self.mapViewControllerBridge.mapViewWillMove(gesture)
}
}
}
- 更新 ContentView 以傳入
mapWillMove
的值
在 MapViewControllerBridge
上宣布新的結案後,請更新 ContentView
來傳遞此新的關閉值。如果在移動事件與手勢有關,請在這個關閉狀態下將狀態 zoomInCenter
切換為 false
。如此一來,當地圖透過手勢移動時,就能重新顯示完整的檢視畫面。
內容檢視
struct ContentView: View {
@State var zoomInCenter: Bool = false
// ...
var body: some View {
// ...
GeometryReader { geometry in
ZStack(alignment: .top) {
// Map
let diameter = zoomInCenter ? geometry.size.width : (geometry.size.height * 2)
MapViewControllerBridge(markers: $markers, selectedMarker: $selectedMarker, onAnimationEnded: {
self.zoomInCenter = true
}, mapViewWillMove: { (isGesture) in
guard isGesture else { return }
self.zoomInCenter = false
})
// ...
}
}
}
}
- 現在就開始執行應用程式吧!
10. 恭喜
恭喜您獲得這樣的成果!您已涵蓋許多領域,希望您從中學到了,您現在可以運用 Maps SDK for iOS 建構自己的 SwiftUI 應用程式。
您學到的內容
- SwiftUI 和 UIKit 之間的差異
- 如何使用 UIViewControllerRepresentable 切換 SwiftUI 和 UIKit
- 如何使用狀態和繫結變更地圖檢視
- 如何使用 Coordinator 將地圖檢視中的事件傳送到 SwiftUI
後續步驟
- Maps SDK for iOS - Maps SDK for iOS 的官方說明文件
- Places SDK for iOS:尋找附近的商家和搜尋點
- maps-sdk-for-ios-samples - GitHub 上的程式碼範例,示範 Maps SDK for iOS 的所有功能。
- SwiftUI - Apple 的 SwiftUI 官方說明文件
- 請回答下列問題,協助我們製作您覺得最實用的內容:
您還想查看其他程式碼研究室嗎?
上方未列出您所需的程式碼研究室嗎?請在這裡提出新的問題。