使用 SwiftUI 建構第一個 3D 地圖

1. 事前準備

本程式碼研究室將說明如何使用 Maps 3D SDK for iOS,在 SwiftUI 中建立 3D 地圖應用程式。

應用程式顯示舊金山的 3D 地圖

您將學會:

  • 如何控制攝影機查看地點,並在地圖上飛行。
  • 如何新增標記和模型
  • 如何繪製線條和多邊形
  • 如何處理使用者點選地點標記的情況。

必要條件

  • 已啟用計費功能的 Google 控制台專案
  • API 金鑰,可視需要限制為 Maps 3D SDK for iOS。
  • 具備使用 SwiftUI 進行 iOS 開發作業的基本知識。

學習內容

  • 設定 Xcode 並使用 Swift Package Manager 導入 SDK
  • 將應用程式設定為使用 API 金鑰
  • 在應用程式中新增基本 3D 地圖
  • 控制攝影機飛往特定地點,並在該地周圍飛行
  • 在地圖中加入標記、線條、多邊形和模型

軟硬體需求

  • Xcode 15 以上版本。

2. 做好準備

如要完成下列啟用步驟,您必須啟用 Maps 3D SDK for iOS。

設定 Google 地圖平台

如果您尚未建立 Google Cloud Platform 帳戶,以及已啟用帳單功能的專案,請參閱「開始使用 Google 地圖平台」指南,建立帳單帳戶和專案。

  1. Cloud 控制台中,點選專案下拉式選單,然後選取要用於本程式碼研究室的專案。

  1. Google Cloud Marketplace 中啟用這個程式碼研究室所需的 Google 地圖平台 API 和 SDK。如要進行這項操作,請按照這部影片這份說明文件中的步驟操作。
  2. 在 Cloud 控制台的「憑證」頁面中產生 API 金鑰。您可以按照這部影片這份說明文件中的步驟操作。所有 Google 地圖平台要求都需要 API 金鑰。

啟用 Maps 3D SDK for iOS

您可以透過控制台的「Google 地圖平台」>「API 和服務」選單連結,找到適用於 iOS 的 Maps 3D SDK。

按一下「啟用」,即可在所選專案中啟用 API。

在 Google 控制台中啟用 Maps 3D SDK

3. 建立基本 SwiftUI 應用程式

注意:您可以在 GitHub 上的程式碼研究室範例應用程式存放區中,找到各個步驟的解決方案程式碼。

在 Xcode 中建立新應用程式。

您可以在 GitHub 的 GoogleMaps3DDemo 資料夾中找到這個步驟的程式碼。

開啟 Xcode 並建立新的應用程式,指定 SwiftUI。

使用套件名稱 com.example.GoogleMaps3DDemo 呼叫應用程式 GoogleMaps3DDemo

將 GoogleMaps3D 程式庫匯入專案

使用 Swift Package Manager 將 SDK 新增至專案。

在 Xcode 專案或工作區中,依序點選「File」>「Add Package Dependencies」。輸入網址 https://github.com/googlemaps/ios-maps-3d-sdk,按下 Enter 鍵即可匯入套件,然後按一下「Add Package」。

在「Choose Package Products」視窗中,確認 GoogleMaps3D 會新增至指定的主要目標。完成後,按一下「新增套件」。

如要驗證安裝作業,請前往目標的「General」窗格。在「架構」、「程式庫」和「嵌入內容」中,您應該會看到已安裝的套件。您也可以查看 Project Navigator 的「Package Dependencies」部分,驗證套件及其版本。

新增 API 金鑰

您可以將 API 金鑰硬式編碼至應用程式,但這不是理想做法。新增設定檔可讓您保密 API 金鑰,並避免將其存入原始碼控管系統。

在專案根目錄中建立新的設定檔

在 Xcode 中,請確認您正在查看專案探索器視窗。在專案根目錄上按一下滑鼠右鍵,然後選取「New File from Template」。捲動至「Configuration Settings File」部分。選取這個選項,然後按一下「下一步」。請將檔案命名為 Config.xcconfig,並確認已選取專案根資料夾。按一下「建立」即可建立檔案。

在編輯器中,將一行文字新增至設定檔,如下所示:MAPS_API_KEY = YOUR_API_KEY

YOUR_API_KEY 替換成您的 API 金鑰。

將這項設定新增至 Info.plist

方法是選取專案根目錄,然後按一下「資訊」分頁。

新增名為 MAPS_API_KEY 的屬性,並將值設為 $(MAPS_API_KEY)

應用程式程式碼範例包含指定此屬性的Info.plist 檔案

新增地圖

開啟名為 GoogleMaps3DDemoApp.swift 的檔案。這是應用程式的進入點和主要導覽選單。

它會呼叫 ContentView(),顯示「Hello World」訊息。

在編輯器中開啟 ContentView.swift

GoogleMaps3D 新增 import 陳述式。

刪除 var body: some View {} 程式碼區塊中的程式碼。在 body 中宣告新的 Map()

初始化 Map 時,所需的最低設定為 MapMode。這個值有兩種可能的值:

  • .hybrid - 包含道路和標籤的衛星圖像,或
  • .satellite - 僅限衛星圖像。

.hybrid為您服務。

您的 ContentView.swift 檔案應如下所示。

import GoogleMaps3D
import SwiftUI

@main
struct ContentView: View {
    var body: some View {
      Map(mode: .hybrid)
    }
}

設定 API 金鑰。

必須在地圖初始化前設定 API 金鑰。

您可以在任何包含地圖的 Viewinit() 事件處理常式中設定 Map.apiKey,您也可以在 GoogleMaps3DDemoApp.swift 中設定,然後再呼叫 ContentView()

GoogleMaps3DDemoApp.swift 中,在 WindowGrouponAppear 事件處理常式中設定 Map.apiKey

從設定檔擷取 API 金鑰

使用 Bundle.main.infoDictionary 存取您在設定檔中建立的 MAPS_API_KEY 設定。

import GoogleMaps3D
import SwiftUI

@main
struct GoogleMaps3DDemoApp: App {
  var body: some Scene {
    WindowGroup {
      ContentView()
    }
    .onAppear {
      guard let infoDictionary: [String: Any] = Bundle.main.infoDictionary else {
        fatalError("Info.plist not found")
      }
      guard let apiKey: String = infoDictionary["MAPS_API_KEY"] as? String else {
        fatalError("MAPS_API_KEY not set in Info.plist")
      }
      Map.apiKey = apiKey
    }
  }
}

建構並執行應用程式,確認應用程式是否載入正確。畫面上應該會顯示地球地圖。

顯示地球的 3D 地圖

4. 使用攝影機控制地圖檢視畫面

建立相機狀態物件

3D 地圖檢視畫面由 Camera 類別控制。在這個步驟中,您將瞭解如何指定位置、高度、航向、傾斜、翻轉和範圍,以自訂地圖檢視畫面。

舊金山的 3D 地圖檢視畫面

建立 Helpers 類別來儲存相機設定

新增名為 MapHelpers.swift 的空白檔案。在新檔案中匯入 GoogleMaps3D,並在 Camera 類別中新增擴充功能。新增名為 sanFrancisco 的變數。將這個變數初始化為新的 Camera 物件。將攝影機放置在 latitude: 37.39, longitude: -122.08 上。

import GoogleMaps3D

extension Camera {
 public static var sanFrancisco: Camera = .init(latitude: 37.39, longitude: -122.08)
}

在應用程式中新增 View

建立名為 CameraDemo.swift 的新檔案。將新 SwiftUI 檢視畫面的基本大綱新增至檔案。

新增名為 camera 且型別為 Camera@State 變數。將其初始化為您剛剛定義的 sanFrancisco 相機。

使用 @State 可將地圖繫結至相機狀態,並做為可靠資料來源。

@State var camera: Camera = .sanFrancisco

變更 Map() 函式呼叫,納入 camera 屬性。使用相機狀態繫結 $camera,將 camera 屬性初始化為相機 @State 物件 (.sanFrancisco)。

import SwiftUI
import GoogleMaps3D

struct CameraDemo: View {
  @State var camera: Camera = .sanFrancisco
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid)
    }
  }
}

在應用程式中加入基本導覽 UI

NavigationView 新增至主要應用程式進入點 GoogleMaps3DDemoApp.swift

這樣一來,使用者就能查看示範影片清單,並點選每部影片來開啟。

編輯 GoogleMaps3DDemoApp.swift 以新增 NavigationView

新增含有兩個 NavigationLink 宣告的 List

第一個 NavigationLink 應開啟 ContentView(),並顯示 Text 說明 Basic Map

第二個 NavigationLink 應會開啟 CameraDemo()

...
      NavigationView {
        List {
          NavigationLink(destination: ContentView()) {
            Text("Basic Map")
          }
          NavigationLink(destination: CameraDemo()) {
            Text("Camera Demo")
          }
        }
      }
...

新增 Xcode 預覽

預覽是 Xcode 的強大功能,可讓您在變更應用程式時查看及互動。

如要新增預覽畫面,請開啟 CameraDemo.swift。在 struct 外部新增 #Preview {} 程式碼區塊。

#Preview {
 CameraDemo()
}

在 Xcode 中開啟或重新整理「Preview」窗格。地圖應顯示舊金山。

設定自訂 3D 檢視畫面

您可以指定其他參數來控制攝影機:

  • heading:從正北指向攝影機的方向,以度為單位。
  • tilt:傾斜角度 (以度為單位),0 代表正上方,90 代表水平方向。
  • roll:相機垂直平面上的滾動角度,以度為單位
  • range:相機與經緯度位置的距離 (以公尺為單位)
  • altitude:相機高於海平面的高度

如果您未提供任何額外參數,系統會使用預設值。

如要讓相機檢視畫面顯示更多 3D 資料,請設定初始參數,以便顯示更靠近且傾斜的檢視畫面。

編輯您在 MapHelpers.swift 中定義的 Camera,以納入 altitudeheadingtiltrollrange 的值

public static var sanFrancisco: Camera = .init(
  latitude: 37.7845812,
  longitude: -122.3660241,
  altitude: 585,
  heading: 288.0,
  tilt: 75.0,
  roll: 0.0,
  range: 100)

建構並執行應用程式,查看及探索新的 3D 檢視畫面。

5. 基本相機動畫

到目前為止,您已使用攝影機指定單一位置,並設定傾斜度、高度、方位和範圍。在這個步驟中,您將瞭解如何透過動畫將這些屬性從初始狀態移至新狀態,進而移動攝影機檢視畫面。

西雅圖的 3D 地圖

飛往某個地點

您將使用 Map.flyCameraTo() 方法,讓攝影機從初始位置動畫轉移至新位置。

flyCameraTo() 方法會採用多個參數:

  • Camera 代表終點位置。
  • duration:動畫的執行時間長度,以秒為單位。
  • trigger:可觀察的物件,會在狀態變更時觸發動畫。
  • completion:動畫完成時會執行的程式碼。

定義要飛往的地點

開啟 MapHelpers.swift 檔案。

定義新的攝影機物件,以便顯示西雅圖。

public static var seattle: Camera = .init(latitude:
47.6210296,longitude: -122.3496903, heading: 149.0, tilt: 77.0, roll: 0.0, range: 4000)

新增按鈕來觸發動畫。

開啟 CameraDemo.swift。在 struct 中宣告新的布林變數。

將其命名為 animate,初始值為 false

@State private var animate: Bool = false

請在 VStack 下方新增 ButtonButton 會啟動地圖動畫。

請為 Button 提供適當的 Text,例如「Start Flying」。

import SwiftUI
import GoogleMaps3D

struct CameraDemo: View {
  @State var camera:Camera = .sanFrancisco
  @State private var animate: Bool = false

  var body: some View {
    VStack{
      Map(camera: $camera, mode: .hybrid)
      Button("Start Flying") {
      }
    }
  }
}

在 Button 結束函式中新增程式碼,切換 animate 變數的狀態。

      Button("Start Flying") {
        animate.toggle()
      }

開始播放動畫。

新增程式碼,在 animate 變數狀態變更時觸發 flyCameraTo() 動畫。

  var body: some View {
    VStack{
      Map(camera: $camera, mode: .hybrid)
        .flyCameraTo(
          .seattle,
          duration: 5,
          trigger: animate,
          completion: {  }
        )
      Button("Start Flying") {
        animate.toggle()
      }
    }
  }

在某個地點附近飛行

您可以使用 Map.flyCameraAround() 方法,在某個位置附近飛行。這個方法會使用多個參數:

  • 定義位置和檢視畫面的 Camera
  • duration 以秒為單位。
  • rounds:重複播放動畫的次數。
  • trigger:會觸發動畫的觀察物件。
  • callback:動畫執行時會執行的程式碼。

定義名為 flyAround 的新 @State 變數,初始值為 false

完成後,請在 flyCameraTo() 方法呼叫後立即新增對 flyCameraAround() 的呼叫。

飛行時間應相對較長,以便順暢地變更檢視畫面。

請務必在 flyCameraTo() 完成時,透過變更觸發事件物件的狀態來觸發 flyCameraAround() 動畫。

您的程式碼應如下所示。

import SwiftUI
import GoogleMaps3D

struct CameraDemo: View {
  @State var camera:Camera = .sanFrancisco
  @State private var animate: Bool = false
  @State private var flyAround: Bool = false

  var body: some View {
    VStack{
      Map(camera: $camera, mode: .hybrid)
        .flyCameraTo(
          .seattle,
          duration: 5,
          trigger: animate,
          completion: { flyAround = true }
        )
        .flyCameraAround(
          .seattle,
          duration: 15,
          rounds: 0.5,
          trigger: flyAround,
          callback: {  }
        )
      Button("Start Flying") {
        animate.toggle()
      }
    }
  }
}

#Preview {
  CameraDemo()
}

預覽或執行應用程式,確認 flyCameraTo() 動畫完成後,相機會在目的地周圍飛行。

6. 在地圖中加入標記。

在這個步驟中,您將瞭解如何在地圖上繪製標記圖釘。

您將建立 Marker 物件並新增至地圖。SDK 會使用預設圖示做為標記。最後,您將調整標記的高度和其他屬性,以變更標記的顯示方式。

顯示地圖標記的 3D 地圖

為 Marker 示範建立新的 SwiftUI 檢視畫面。

在專案中新增 Swift 檔案。將其命名為 MarkerDemo.swift

新增 SwiftUI 檢視畫面的輪廓,並按照 CameraDemo 中的做法初始化地圖。

import SwiftUI
import GoogleMaps3D

struct MarkerDemo: View {
  @State var camera: Camera = .sanFrancisco
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid)
    }
  }
}

初始化 Marker 物件

宣告名為 mapMarker 的新標記變數。MarkerDemo.swiftstruct 程式碼區塊的頂端。

將定義放在 camera 宣告下方的行中。這個範例程式碼會初始化所有可用的屬性。

  @State var mapMarker: Marker = .init(
    position: .init(
      latitude: 37.8044862,
      longitude: -122.4301493,
      altitude: 0.0),
    altitudeMode: .absolute,
    collisionBehavior: .required,
    extruded: false,
    drawsWhenOccluded: true,
    sizePreserved: true,
    zIndex: 0,
    label: "Test"
  )

在地圖中加入標記。

如要繪製標記,請將標記新增至在建立地圖時呼叫的結束函式。

struct MarkerDemo: View {
  @State var camera: Camera = .sanFrancisco
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        mapMarker
      }
    }
  }
}

將新的 NavigationLink 新增至 GoogleMaps3DDemoApp.swift,目的地為 MarkerDemo()Text 則描述為「標記示範」。

...
      NavigationView {
        List {
          NavigationLink(destination: Map()) {
            Text("Basic Map")
          }
          NavigationLink(destination: CameraDemo()) {
            Text("Camera Demo")
          }
          NavigationLink(destination: MarkerDemo()) {
            Text("Marker Demo")
          }
        }
      }
...

預覽及執行應用程式

重新整理預覽畫面或執行應用程式,即可查看標記。

凸出標記

您可以使用 altitudealtitudeMode,將標記置於地面或 3D 網格上方。

MarkerDemo.swift 中的 mapMarker 宣告複製到名為 extrudedMarker 的新 Marker 變數。

altitude 設定非零值,50 就足夠了。

altitudeMode 變更為 .relativeToMesh,並將 extruded 設為 true。使用此處程式碼片段中的 latitudelongitude,將標記放在摩天大樓頂端。

  @State var extrudedMarker: Marker = .init(
    position: .init(
      latitude: 37.78980534,
      longitude:  -122.3969349,
      altitude: 50.0),
    altitudeMode: .relativeToMesh,
    collisionBehavior: .required,
    extruded: true,
    drawsWhenOccluded: true,
    sizePreserved: true,
    zIndex: 0,
    label: "Extruded"
  )

再次執行或預覽應用程式。標記應顯示在 3D 建築物頂端。

7. 在地圖中加入模型。

Model 的新增方式與 Marker 相同。您需要的模型檔案必須可透過網址存取,或可在專案中新增為本機檔案。在這個步驟中,我們會使用本機檔案,您可以從本程式碼研究室的 GitHub 存放區下載。

舊金山的 3D 地圖,以及熱氣球模型

將模型檔案新增至專案

在 Xcode 專案中建立名為 Models 的新資料夾。

從 GitHub 範例應用程式存放區下載模型。將其拖曳至 Xcode 專案檢視畫面中的新資料夾,即可將其新增至專案。

請務必將目標設為應用程式的主目標。

請檢查專案的「Build Phases」>「Copy Bundle Resources」設定。模型檔案應列於複製至組合的資源清單中。如果找不到,請按一下「+」新增。

將模型新增至應用程式。

建立名為 ModelDemo.swift 的新 SwiftUI 檔案。

如同先前步驟所述,為 SwiftUIGoogleMaps3D 新增 import 陳述式。

bodyVStack 中宣告 Map

import SwiftUI
import GoogleMaps3D

struct ModelDemo: View {
  @State var camera: Camera = .sanFrancisco
  
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        
      }
    }
  }
}

從套件取得模型路徑。請在 struct 外部新增這段程式碼。

private let fileUrl = Bundle.main.url(forResource: "balloon", withExtension: "glb")

在結構體中宣告模型的變數。

如未提供 fileUrl,請提供預設值。

  @State var balloonModel: Model = .init(
    position: .init(
      latitude: 37.791376,
      longitude: -122.397571,
      altitude: 300.0),
    url: URL(fileURLWithPath: fileUrl?.relativePath ?? ""),
    altitudeMode: .absolute,
    scale: .init(x: 5, y: 5, z: 5),
    orientation: .init(heading: 0, tilt: 0, roll: 0)
  )

3. 在地圖中使用模型。

如同新增 Marker 一樣,只要在 Map 宣告中提供 Model 的參照即可。

  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        balloonModel
      }
    }
  }

預覽及執行應用程式

將新的 NavigationLink 新增至 GoogleMaps3DDemoApp.swift,目的地為 ModelDemo()Text 為「Model Demo」。

...
          NavigationLink(destination: ModelDemo()) {
            Text("Model Demo")
          }
...

重新整理預覽畫面或執行應用程式,即可查看模型。

8. 在地圖上繪製線條和多邊形。

在這個步驟中,您將瞭解如何在 3D 地圖中加入線條和多邊形形狀。

為求簡單,您將形狀定義為 LatLngAltitude 物件的陣列。在實際應用程式中,資料可能會從檔案、API 呼叫或資料庫載入。

舊金山的 3D 地圖,顯示兩個多邊形和一條折線

建立一些形狀物件來管理形狀資料。

將新的 Camera 定義新增至 MapHelpers.swift,以便查看舊金山市區。

  public static var downtownSanFrancisco: Camera = .init(latitude: 37.7905, longitude: -122.3989, heading: 25, tilt: 71, range: 2500) 

在專案中新增名為 ShapesDemo.swift 的檔案。新增名為 ShapesDemostruct,實作 View 通訊協定,並在其中新增 body

struct ShapesDemo: View {
  @State var camera: Camera = .downtownSanFrancisco

  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {

      }
    }
  }
}

您用來管理形狀資料的類別是 PolylinePolygon。開啟 ShapesDemo.swift,並按照下列方式將其新增至 struct

var polyline: Polyline = .init(coordinates: [
    LatLngAltitude(latitude: 37.80515638571346, longitude: -122.4032569467164, altitude: 0),
    LatLngAltitude(latitude: 37.80337073509504, longitude: -122.4012878349353, altitude: 0),
    LatLngAltitude(latitude: 37.79925208843463, longitude: -122.3976697250461, altitude: 0),
    LatLngAltitude(latitude: 37.7989102378512, longitude: -122.3983408725656, altitude: 0),
    LatLngAltitude(latitude: 37.79887832784348, longitude: -122.3987094864192, altitude: 0),
    LatLngAltitude(latitude: 37.79786443410338, longitude: -122.4066878788802, altitude: 0),
    LatLngAltitude(latitude: 37.79549248916587, longitude: -122.4032992702785, altitude: 0),
    LatLngAltitude(latitude: 37.78861484290265, longitude: -122.4019489189814, altitude: 0),
    LatLngAltitude(latitude: 37.78618687561075, longitude: -122.398969592545, altitude: 0),
    LatLngAltitude(latitude: 37.7892310309145, longitude: -122.3951458683092, altitude: 0),
    LatLngAltitude(latitude: 37.7916358762409, longitude: -122.3981969390652, altitude: 0)
  ])
  .stroke(GoogleMaps3D.Polyline.StrokeStyle(
    strokeColor: UIColor(red: 0.09803921568627451, green: 0.403921568627451, blue: 0.8235294117647058, alpha: 1),
    strokeWidth: 10.0,
    outerColor: .white,
    outerWidth: 0.2
    ))
  .contour(GoogleMaps3D.Polyline.ContourStyle(isGeodesic: true))

  var originPolygon: Polygon = .init(outerCoordinates: [
    LatLngAltitude(latitude: 37.79165766856578, longitude:  -122.3983762901255, altitude: 300),
    LatLngAltitude(latitude: 37.7915324439261, longitude:  -122.3982171091383, altitude: 300),
    LatLngAltitude(latitude: 37.79166617650914, longitude:  -122.3980478493319, altitude: 300),
    LatLngAltitude(latitude: 37.79178986470217, longitude:  -122.3982041104199, altitude: 300),
    LatLngAltitude(latitude: 37.79165766856578, longitude:  -122.3983762901255, altitude: 300 )
  ],
  altitudeMode: .relativeToGround)
  .style(GoogleMaps3D.Polygon.StyleOptions(fillColor:.green, extruded: true) )

  var destinationPolygon: Polygon = .init(outerCoordinates: [
      LatLngAltitude(latitude: 37.80515661739527, longitude:  -122.4034307490334, altitude: 300),
      LatLngAltitude(latitude: 37.80503794515428, longitude:  -122.4032633416024, altitude: 300),
      LatLngAltitude(latitude: 37.80517850164195, longitude:  -122.4031056058006, altitude: 300),
      LatLngAltitude(latitude: 37.80529346901115, longitude:  -122.4032622466595, altitude: 300),
      LatLngAltitude(latitude: 37.80515661739527, longitude:  -122.4034307490334, altitude: 300 )
  ],
  altitudeMode: .relativeToGround)
  .style(GoogleMaps3D.Polygon.StyleOptions(fillColor:.red, extruded: true) )

請注意所使用的初始化參數。

  • altitudeMode: .relativeToGround 可用於將多邊形推擠至地面以上特定高度。
  • altitudeMode: .clampToGround 可讓折線沿著地球表面的形狀移動。
  • .init() 呼叫後,將方法呼叫鏈結至 styleOptions(),藉此在 Polygon 物件上設定樣式

在地圖上新增形狀

就像先前的步驟一樣,您可以直接將形狀新增至 Map 關閉式。在 VStack 中建立 Map

...
  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid) {
        polyline
        originPolygon
        destinationPolygon
      }
    }
  }
...

預覽及執行應用程式

新增預覽程式碼,並在 Xcode 的「Preview」窗格中檢查應用程式。

#Preview {
  ShapesDemo()
}

如要執行應用程式,請將新的 NavigationLink 新增至 GoogleMaps3DDemoApp.swift,以便開啟新的示範 View。

...
          NavigationLink(destination: ShapesDemo()) {
            Text("Shapes Demo")
          }
...

執行應用程式,並查看您新增的形狀。

9. 處理 Place 標記上的輕觸事件

在這個步驟中,您將瞭解如何回應使用者輕觸地點標記的動作。

地圖顯示含有地點 ID 的彈出式視窗

注意:如要在地圖上顯示地點標記,您必須將 MapMode 設為 .hybrid

如要處理輕觸動作,您必須實作 Map.onPlaceTap 方法。

onPlaceTap 事件提供 PlaceTapInfo 物件,您可以透過該物件取得輕觸地點標記的地點 ID。

您可以使用 Place ID 搭配 Places SDKPlaces API 查詢更多詳細資料。

新增 Swift View

將下列程式碼新增至名為 PlaceTapDemo.swift 的新 Swift 檔案。

import GoogleMaps3D
import SwiftUI

struct PlaceTapDemo: View {
  @State var camera: Camera = .sanFrancisco
  @State var isPresented = false
  @State var tapInfo: PlaceTapInfo?

  var body: some View {
    Map(camera: $camera, mode: .hybrid)
      .onPlaceTap { tapInfo in
        self.tapInfo = tapInfo
        isPresented.toggle()
      }
      .alert(
        "Place tapped - \(tapInfo?.placeId ?? "nil")",
        isPresented: $isPresented,
        actions: { Button("OK") {} }
      )
  }
}
#Preview {
  PlaceTapDemo()
}

預覽及執行應用程式

開啟「預覽」窗格,即可預覽應用程式。

如要執行應用程式,請將新的 NavigationLink 新增至 GoogleMaps3DDemoApp.swift

...
          NavigationLink(destination: PlaceTapDemo()) {
            Text("Place Tap Demo")
          }
...

10. (選用) 進一步瞭解

進階相機動畫

某些用途需要沿著位置或攝影機狀態序列/清單流暢地播放動畫,例如飛行模擬器或重播健行或跑步畫面。

在這個步驟中,您將瞭解如何從檔案載入位置清單,並依序為每個位置製作動畫。

前往因斯布魯克的 3D 地圖檢視畫面

載入包含位置序列的檔案。

GitHub 範例應用程式存放區下載 flightpath.json

在 Xcode 專案中建立名為 JSON 的新資料夾。

flightpath.json 拖曳至 Xcode 中的 JSON 資料夾。

將目標設為應用程式的主要目標。確認專案的「複製套件資源」設定包含此檔案。

在應用程式中建立兩個名為 FlightPathData.swiftFlightDataLoader.swift 的新 Swift 檔案。

將下列程式碼複製到應用程式中。這段程式碼會建立結構體和類別,用於讀取名為「flighpath.json」的本機檔案,並將其解碼為 JSON。

FlightPathDataFlightPathLocation 結構體會以 Swift 物件呈現 JSON 檔案中的資料結構。

FlightDataLoader 類別會讀取檔案中的資料並進行解碼。採用 ObservableObject 通訊協定,讓應用程式可觀察資料的變更。

剖析的資料會透過已發布的資源公開。

FlightPaths.swift

import GoogleMaps3D

struct FlightPathData: Decodable {
  let flight: [FlightPathLocation]
}

struct FlightPathLocation: Decodable {
  let timestamp: Int64
  let latitude: Double
  let longitude: Double
  let altitude: Double
  let bearing: Double
  let speed: Double
}

FlightDataLoader.swift

import Foundation

public class FlightDataLoader : ObservableObject {

  @Published var flightPathData: FlightPathData = FlightPathData(flight:[])
  @Published var isLoaded: Bool = false

  public init() {
    load("flightpath.json")
  }
  
  public func load(_ path: String) {
    if let url = Bundle.main.url(forResource: path, withExtension: nil){
      if let data = try? Data(contentsOf: url){
        let jsondecoder = JSONDecoder()
        do{
          let result = try jsondecoder.decode(FlightPathData.self, from: data)
          flightPathData = result
          isLoaded = true
        }
        catch {
          print("Error trying to load or parse the JSON file.")
        }
      }
    }
  }
}

為攝影機沿著每個位置移動加入動畫效果

如要在一系列步驟之間為攝影機加入動畫效果,請使用 KeyframeAnimator

每個 Keyframe 都會以 CubicKeyframe 的形式建立,因此攝影機狀態的變更會以流暢的動畫呈現。

使用 flyCameraTo() 會導致檢視區塊在各位置之間「彈跳」。

首先,請在 MapHelpers.swift 中宣告名為「innsbruck」的攝影機。

public static var innsbruck: Camera = .init(
  latitude: 47.263,
  longitude: 11.3704,
  altitude: 640.08,
  heading: 237,
  tilt: 80.0,
  roll: 0.0,
  range: 200)

接著,在名為 FlyAlongRoute.swift 的新檔案中設定新的 View。

匯入 SwiftUIGoogleMaps3D。在 VStack 中新增 MapButton。設定 Button 來切換布林值 animation 變數的狀態。

FlightDataLoader 宣告 State 物件,這會在初始化時載入 JSON 檔案。

import GoogleMaps3D
import SwiftUI

struct FlyAlongRoute: View {
  @State private var camera: Camera = .innsbruck
  @State private var flyToDuration: TimeInterval = 5
  @State var animation: Bool = true

  @StateObject var flightData: FlightDataLoader = FlightDataLoader()

  var body: some View {
    VStack {
      Map(camera: $camera, mode: .hybrid)
      Button("Fly Along Route"){
        animation.toggle()
      }
    }
  }
}

建立主要影格

基本程序是建立可在動畫序列中傳回新影格的函式。每個新影格都會定義動畫師要為其製作動畫的下一個攝影機狀態。建立此函式後,請依序使用檔案中的每個位置呼叫該函式。

FlyAlongRoute 結構體中新增兩個函式。makeKeyFrame 函式會傳回含有攝影機狀態的 CubicKeyframemakeCamera 函式會擷取飛行資料序列中的步驟,並傳回代表該步驟的 Camera 物件。

func makeKeyFrame(step: FlightPathLocation) -> CubicKeyframe<Camera> {
  return CubicKeyframe(
    makeCamera(step: step),
    duration: flyToDuration
  )
}

func makeCamera(step: FlightPathLocation) -> Camera {
  return .init(
    latitude: step.latitude,
    longitude: step.longitude,
    altitude: step.altitude,
    heading: step.bearing,
    tilt: 75,
    roll: 0,
    range: 200
  )
}

將動畫組合在一起

Map 初始化後呼叫 keyframeAnimator,並設定初始值。

您需要根據飛行路徑的第一個位置,設定初始攝影機狀態。

動畫應根據變數變更狀態觸發。

keyframeAnimator 內容應為地圖。

實際的關鍵影格清單會透過循環處理飛行路徑中的每個位置產生。

   VStack {
      Map(camera: $camera, mode: .hybrid)
        .keyframeAnimator(
          initialValue: makeCamera(step: flightData.flightPathData.flight[0]),
          trigger: animation,
          content: { view, value in
            Map(camera: .constant(value), mode: .hybrid)
          },
          keyframes: { _ in
            KeyframeTrack(content: {
              for i in  1...flightData.flightPathData.flight.count-1 {
                makeKeyFrame(step: flightData.flightPathData.flight[i])
              }
            })
          }
        )
   }

預覽及執行應用程式。

開啟「預覽」窗格,預覽檢視畫面。

將新的 NavigationLink 新增至 GoogleMaps3DDemoApp.swift,並設定 FlightPathDemo() 做為目的地,然後執行應用程式來試用。

11. 恭喜

您已成功建構應用程式,該應用程式可執行以下操作:

  • 在應用程式中加入基本 3D 地圖。
  • 在地圖中加入標記、線條、多邊形和模型。
  • 實作程式碼,控制攝影機飛越地圖和特定位置。

您學到的內容

  • 如何將 GoogleMaps3D 套件新增至 Xcode SwiftUI 應用程式。
  • 如何使用 API 金鑰和預設檢視畫面初始化 3D 地圖。
  • 如何在地圖中加入標記、3D 模型、線條和多邊形。
  • 如何控制攝影機,讓攝影機動畫移動到其他位置。
  • 如何處理地點標記的點擊事件。

後續步驟

  • 如要進一步瞭解如何使用 Maps 3D SDK for iOS,請參閱開發人員指南
  • 請回答下列問卷調查,協助我們製作最實用的內容:

您想參考哪些其他程式碼研究室?

地圖上的資料視覺化 進一步瞭解如何自訂地圖樣式 建構地圖中的 3D 互動功能