使用 SwiftUI 构建您的首个 3D 地图

1. 准备工作

此 Codelab 介绍了如何使用 Maps 3D SDK for iOS 在 SwiftUI 中创建 3D 地图应用。

显示旧金山 3D 地图的应用

您会了解到以下内容:

  • 如何控制相机查看地点和在 Google 地图上飞行。
  • 如何添加标记和模型
  • 如何绘制线条和多边形
  • 如何处理用户对地点标记的点击。

前提条件

  • 启用了结算功能的 Google 控制台项目
  • API 密钥,可选择限制为仅适用于 iOS 版 Maps 3D SDK。
  • 具备使用 SwiftUI 进行 iOS 开发的基础知识。

您将执行的操作

  • 设置 Xcode 并使用 Swift Package Manager 引入 SDK
  • 将应用配置为使用 API 密钥
  • 向应用添加基本 3D 地图
  • 控制摄像头飞往特定位置并在其周围飞行
  • 向地图添加标记、线条、多边形和模型

所需条件

  • Xcode 15 或更高版本。

2. 进行设置

为了完成以下启用步骤,您需要启用 Maps 3D SDK for iOS。

设置 Google Maps Platform

如果您还没有已启用结算功能的 Google Cloud Platform 账号和项目,请参阅 Google Maps Platform 使用入门指南,创建结算账号和项目。

  1. Cloud Console 中,点击项目下拉菜单,选择要用于此 Codelab 的项目。

  1. Google Cloud Marketplace 中启用此 Codelab 所需的 Google Maps Platform API 和 SDK。为此,请按照此视频此文档中的步骤操作。
  2. 在 Cloud Console 的凭据页面中生成 API 密钥。您可以按照此视频此文档中的步骤操作。向 Google Maps Platform 发出的所有请求都需要 API 密钥。

启用 iOS 版 Maps 3D SDK

您可以通过控制台中的“Google Maps Platform”>“API 和服务”菜单链接找到 Maps 3D SDK for iOS。

点击“启用”以在所选项目中启用该 API。

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

3. 创建一个基本的 SwiftUI 应用

注意:您可以在 GitHub 上的 Codelab 示例应用代码库中找到每一步的解决方案代码。

在 Xcode 中创建新应用。

此步骤的代码可以在 GitHub 上的 GoogleMaps3DDemo 文件夹中找到。

打开 Xcode 并创建一个新应用。指定 SwiftUI。

将应用命名为 GoogleMaps3DDemo,软件包名称为 com.example.GoogleMaps3DDemo

将 GoogleMaps3D 库导入您的项目

使用 Swift Package Manager 将 SDK 添加到您的项目中。

在 Xcode 项目或工作区中,依次选择“File”(文件)>“Add Package Dependencies”(添加软件包依赖项)。输入 https://github.com/googlemaps/ios-maps-3d-sdk 作为网址,按 Enter 键提取软件包,然后点击“Add Package”(添加软件包)。

在“选择软件包商品”窗口中,验证 GoogleMaps3D 是否会添加到您指定的主要目标。完成后,点击“添加软件包”。

如需验证安装情况,请前往目标的“常规”窗格。在“框架、库和嵌入内容”中,您应该会看到已安装的软件包。您还可以查看 Project Navigator 的“软件包依赖项”部分,以验证软件包及其版本。

添加您的 API 密钥

您可以将 API 密钥硬编码到应用中,但这不是一种好做法。通过添加配置文件,您可以保留 API 密钥的秘密性,并避免将其签入源代码控制系统。

在项目根文件夹中创建新的配置文件

在 Xcode 中,确保您正在查看项目浏览器窗口。右键点击项目根目录,然后选择“New File from Template”。滚动到“配置设置文件”。选择此选项,然后点击“下一步”。为文件命名为 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 密钥,然后才能初始化 Map。

为此,您可以在包含地图的任何 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 可将 Map 绑定到相机状态,并将其用作可信来源。

@State var camera: Camera = .sanFrancisco

更改 Map() 函数调用以包含 camera 属性。使用相机状态绑定 $cameracamera 属性初始化为相机 @State 对象 (.sanFrancisco)。

import SwiftUI
import GoogleMaps3D

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

向应用添加基本导航界面

向应用主入口点 GoogleMaps3DDemoApp.swift 添加 NavigationView

这样,用户就可以查看演示版列表,并点击每个演示版将其打开。

修改 GoogleMaps3DDemoApp.swift 以添加新的 NavigationView

添加一个包含两个 NavigationLink 声明的 List

第一个 NavigationLink 应打开带有 Text 说明 Basic MapContentView()

第二个 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 中打开或刷新“预览”窗格。地图上应显示旧金山。

设置自定义 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,例如“开始飞行”。

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 地图

为标记演示创建新的 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"
  )

将标记添加到地图中。

如需绘制标记,请将其添加到创建 Map 时调用的闭包中。

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

GoogleMaps3DDemoApp.swift 添加一个新的 NavigationLink,将其目标设为 MarkerDemo(),并使用 Text 将其描述为“Marker Demo”。

...
      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 相同。您需要一个可通过网址访问或添加为项目中的本地文件的模型文件。在此步骤中,我们将使用一个本地文件,您可以从此 Codelab 的 GitHub 代码库下载该文件。

旧金山的 3D 地图,其中包含热气球模型

将模型文件添加到项目中

在 Xcode 项目中创建一个名为 Models 的新文件夹。

从 GitHub 示例应用代码库下载模型。将其拖动到 Xcode 项目视图中的新文件夹,将其添加到项目中。

请务必将该目标设置为应用的主要目标。

检查项目的“构建阶段”>“复制软件包资源”设置。模型文件应位于复制到软件包的资源列表中。如果没有,请点击“+”进行添加。

将模型添加到您的应用。

创建一个名为 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
      }
    }
  }

预览和运行应用

GoogleMaps3DDemoApp.swift 添加一个新的 NavigationLink,目标为 ModelDemo()Text 为“Model Demo”。

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

刷新预览或运行应用以查看模型。

8. 在地图上绘制线条和多边形。

在此步骤中,您将学习如何向 3D 地图添加线条和多边形形状。

为简单起见,您将形状定义为 LatLngAltitude 对象的数组。在真实应用中,数据可能会从文件、API 调用或数据库加载。

旧金山的 3D 地图,显示了两个多边形和一条多段线

创建一些形状对象来管理形状数据。

MapHelpers.swift 添加一个新的 Camera 定义,用于查看旧金山市区。

  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 用于将多边形 extrusion 到地面上方特定的高度。
  • 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()
}

如需运行应用,请向 GoogleMaps3DDemoApp.swift 添加一个新的 NavigationLink,以打开新的演示版 View。

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

运行应用并探索您添加的形状。

9. 处理地点标记上的点按事件

在此步骤中,您将学习如何响应用户点按地点标记的操作。

显示包含地点 ID 的弹出式窗口的地图

注意:如需在地图上查看地点标记,您需要将 MapMode 设置为 .hybrid

如需处理点按,需要实现 Map.onPlaceTap 方法。

onPlaceTap 事件会提供一个 PlaceTapInfo 对象,您可以通过该对象获取用户点按的地点标记的地点 ID。

您可以使用地点 ID 通过 Places SDKPlaces API 查找更多详细信息。

添加新的 Swift 视图

将以下代码添加到名为 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()
}

预览和运行应用

打开“预览”窗格以预览应用。

如需运行应用,请向 GoogleMaps3DDemoApp.swift 添加新的 NavigationLink

...
          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 结构体将 JSON 文件中的数据结构表示为 Swift 对象。

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 会返回一个包含相机状态的 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 内容应为 Map。

实际的关键帧列表是通过循环遍历飞行路径中的每个位置生成的。

   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])
              }
            })
          }
        )
   }

预览和运行应用。

打开“预览”窗格以预览视图。

GoogleMaps3DDemoApp.swift 添加一个目的地为 FlightPathDemo() 的新 NavigationLink,然后运行应用进行试用。

11. 恭喜

您已成功构建一款具备以下特征的应用:

  • 向应用添加基本 3D 地图。
  • 向地图添加标记、线条、多边形和模型。
  • 实现代码以控制相机在特定位置周围和地图上飞行。

要点回顾

  • 如何将 GoogleMaps3D 软件包添加到 Xcode SwiftUI 应用。
  • 如何使用 API 密钥和默认视图初始化 3D 地图。
  • 如何向地图添加标记、3D 模型、线条和多边形。
  • 如何控制相机以呈现移动到其他位置的动画效果。
  • 如何处理地点标记上的点击事件。

后续操作

  • 如需详细了解如何使用 iOS 版 Maps 3D SDK,请参阅开发者指南
  • 请回答以下调查问卷,帮助我们为您制作最为有用的内容:

您还想查看其他哪些 Codelab?

地图上的数据可视化 更多关于自定义地图样式的信息 在地图中构建 3D 交互