使用 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 金鑰。

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

在 GoogleMaps3DDemoApp.swift 中,在 WindowGroup 的 onAppear 事件處理常式中設定 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,以納入 altitude、heading、tilt、roll 和 range 的值

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 下方新增 Button。Button 會啟動地圖動畫。

請為 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.swift 中 struct 程式碼區塊的頂端。

將定義放在 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")
          }
        }
      }
...

預覽及執行應用程式

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

凸出標記

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

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

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

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

  @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 檔案。

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

在 body 的 VStack 中宣告 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 的檔案。新增名為 ShapesDemo 的 struct,實作 View 通訊協定,並在其中新增 body。

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

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

      }
    }
  }
}

您用來管理形狀資料的類別是 Polyline 和 Polygon。開啟 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 SDK 或 Places 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.swift 和 FlightDataLoader.swift 的新 Swift 檔案。

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

FlightPathData 和 FlightPathLocation 結構體會以 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。

匯入 SwiftUI 和 GoogleMaps3D。在 VStack 中新增 Map 和 Button。設定 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 函式會傳回含有攝影機狀態的 CubicKeyframe。makeCamera 函式會擷取飛行資料序列中的步驟,並傳回代表該步驟的 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 互動功能