自定义导航布局

展示 iOS 上自定义导航布局的动画示例

注意:本文档对类、协议和 API 使用 Swift 命名规范。

使用布局自定义功能,控制 Google 提供的视图和您自己的自定义业务视图的视图层次结构和屏幕位置。

请实现布局委托来管理整个屏幕的布局,而不是将自定义视图添加到预定义的布局槽中。在导航状态转换期间,您的布局委托会收到 Google 提供的组件,例如转向卡片、预计到达时间卡片和按钮。使用标准 iOS 自动布局限制条件,将这些元素与您自己的自定义内容并排放置。

使用此框架可在屏幕上精确放置自定义商家信息(例如订单状态或取餐说明),同时防止视图重叠。

布局自定义功能的运作方式

布局自定义使用委托设计模式。SDK 不会自动在屏幕上绘制界面组件或为其定位,而是直接将它们传递给您编写的自定义类:布局委托。

如需自定义布局,请创建一个符合 NavigationLayoutDelegate 协议的类,并将一个实例传递给 GMSMapView 的 setLayoutDelegate(_:) 方法。每当导航状态发生转换(例如从基本地图切换到主动路线规划导航)时,SDK 都会对您的委托执行回调方法,并提供适用于该特定状态的界面组件。

为了提供顺畅、集成的用户体验,此框架在应用和 SDK 之间建立了明确的责任划分。使用布局委托可自定义以下内容:

  • 构建视图层次结构:选择要为每个界面状态添加到屏幕中的 Google 组件和自定义业务视图。
  • 定位每个元素:设置精确的屏幕锚点、外边距和布局位置。避免对 Google 组件应用自定义宽度或高度限制,因为 Google 组件会计算自己的内部尺寸。
  • 框定基本地图:使用视口组件的坐标定义地图镜头的可见边界。
  • 分层显示屏幕:决定自定义视图是浮动在 Google 内置控件的上方、下方还是旁边。

同时,您无法使用布局委托自定义以下 Google 组件:

  • 组件尺寸:Google 提供的组件的大小和内部尺寸,由 SDK 自动计算。
  • 触发条件:根据实时路线数据,何时显示动态提醒或提示。

实施原则

编写布局委托时,请牢记以下规则,以防止出现布局 bug 或运行时崩溃:

  • 默认情况下,屏幕为空:只有当您的委托明确将 Google 提供的组件添加到视图层次结构并对其进行定位时,这些组件才会显示。
  • 不支持旧版 API:控制旧版基于槽的布局模型的属性和方法将不受支持,并且在自定义委托处于活跃状态时可能无法按预期运行。
  • 请勿修改内部视图结构:请勿遍历或修改 Google 提供的组件(例如转向卡片或预计到达时间卡片)的视图层次结构。由于这些内部视图层次结构是底层实现细节,因此它们可能会在不同的 SDK 版本中发生变化。修改这些资源可能会导致您的布局在未来的 SDK 更新期间损坏。

旧版 API 兼容性

为了在使用自定义布局委托时获得可靠的布局行为,请避免使用以下基于槽的旧版 API,这些 API 计划弃用。如需迁移现有应用,请将这些 API 的使用替换为自定义布局委托中的代码:

显示基于槽位的旧版 API

旧版 API 布局委托替换
isNavigationHeaderEnabled 读取 startGuidance(navMap:newState:oldState:) 中的 turnCard 视图。
isNavigationFooterEnabled 读取 startGuidance(navMap:newState:oldState:) 中的 etaCard 视图。
isNavigationTripProgressBarEnabled 读取 startGuidance(navMap:newState:oldState:) 中的 tripProgressBar 视图。
isNavigationReportIncidentButtonEnabled 在 buttons 数组中找到 .reporting 按钮。
setHeaderAccessoryView(_:) 和 invalidateLayout(forAccessoryView:) 将您自己的视图添加到布局中,并将其锚定到 turnCard。
padding和paddingAdjustmentBehavior 调整 viewport 视图,使相机位于取景框内。
navigationHeaderLayoutGuide 将自定义视图直接锚定到 turnCard。
navigationFooterLayoutGuide 将自定义视图直接锚定到 etaCard。
bottomTrailingButtonsLayoutGuide 将按钮直接锚定到 etaCard 或容器辅助线。
bottomTrailingButtonsAlternateLayoutGuide 将按钮直接锚定到 .reporting 按钮或 wideMode 约束条件。
navigatorWillPresentPrompt(_:) 和 navigatorDidDismissPrompt(_:) 依赖于 NavigationLayoutDelegate 提示回调,例如 showPrompt(navMap:newPrompt:)。

设置自定义布局委托后,旧版布局指南(例如 navigationHeaderLayoutGuide 或 bottomTrailingButtonsLayoutGuide)和标题辅助视图方法将失效。而是将您的视图锚定到 Google 提供的视图。

停用导航界面后,GMSUISettings 的 compassButton 属性仍会控制罗盘。启用导航功能后,您的委托会决定是否显示 buttons 数组中的指南针。

基本功能版核对清单

请遵循以下基本步骤和要求,成功实现布局委托:

  • 在启用导航之前设置委托:在通过 isNavigationEnabled 或 enableNavigation(with:) 启用导航之前,请在 GMSMapView 上调用 setLayoutDelegate(_:)。如果已启用导航功能,或者您为 CarPlay 屏幕创建了地图视图,该方法会抛出错误。地图视图会保留对委托的强引用。如需恢复内置布局,请在停用导航功能时传递 nil。

    // Set the layout delegate before you enable navigation.
    do {
      try mapView.setLayoutDelegate(MyLayoutDelegate())
    } catch {
      print("Couldn't set the layout delegate: \(error)")
    }
    mapView.isNavigationEnabled = true
        
  • 附加强制性视图:您必须在所有导航状态下将 Google 徽标 (googleLogo) 和视口 (viewport) 添加到布局容器中。

  • 遵循固定尺寸:SDK 会确定 Google 提供的多个组件的尺寸。请勿对以下元素应用自定义宽度或高度限制:

    • 精细导航卡片
    • 预计到达时间卡片
    • 收到的提示
    • Google 徽标
    • 速度微件
  • 将元素添加为子视图:将 Google 提供的视图直接添加为自定义容器视图或 GMSMapView 本身的子视图。

  • 使用布局指南进行对齐:Google 提供的视图包含内置视觉外边距。如需将自定义元素与 Google 提供的组件完美对齐,请将视图限制为 Google 视图的 layoutMarginsGuide 属性。

  • 在状态退出时清理:在 stopGuidance(navMap:newState:oldState:) 中,移除处于活动状态的引导视图并停用其约束条件,然后再恢复导航就绪布局。在 disableNavigationUI(navMap:oldState:) 中,移除您添加的所有视图和布局指南,包括屏幕上的所有提示。这样可以防止一个状态的视图与下一个状态的视图重叠。

  • 负责无障碍功能和从右到左支持:Google 提供的视图符合无障碍标准,但您的委托人负责整体布局的无障碍功能。如需支持从右到左书写的语言,请使用前导和尾随锚点,而不是左侧和右侧锚点。

  • 避免使用旧版布局 API:当自定义委托处于活跃状态时,请勿在 GMSUISettings 上使用旧版布局修改属性,也不要在 GMSMapView 上使用旧版布局指南。附加自定义委托时,SDK 可能会忽略这些旧版通话。

界面状态和 Google 组件

发生导航状态转换时,SDK 会将只读 NavigationUIState 对象传递给委托回调。此对象将当前布局配置标志与渲染屏幕所需的 Google 组件(例如 UIView 实例)捆绑在一起。

布局委托负责管理四种操作状态下的视图层次结构。 以下状态图说明了 SDK 如何在导航状态之间转换,以及它在委托上执行哪些回调方法:

NavigationLayoutDelegate 生命周期状态转换图,显示了“非导航”“导航就绪”和“主动引导”状态之间的转换。

提供给委托对象的具体 Google 组件取决于导航生命周期的当前阶段。

必需组件(所有导航状态)

无论当前处于哪个导航阶段,您都必须在视图层次结构中添加以下 Google 组件,并将其放置在适当位置,使其保持可见:

  • Google 徽标 (googleLogo):此组件显示强制性 Google 地图徽标。如果重新居中按钮处于启用状态,当司机滚动地图使其偏离车辆时,徽标会自动转换为此按钮。因此,徽标的放置位置决定了重新居中按钮的显示位置。建议将徽标放置在布局的左下角,以符合标准地图界面的预期。

  • 视口 (viewport):一个不可见的 UIView,用于定义摄像头的取景边界。视口的位置决定了 SDK 将车辆 V 形图标居中并绘制有效路线的确切位置。调整视口位置,使其覆盖屏幕上未被遮挡的开放区域,从而安全地避开自定义底部动作条等不透明叠加层。

在导航就绪状态下,界面会保持精简,以便用户专注于基本地图。您的 NavigationReadyUIState 对象可让您访问以下内容:

  • buttons:为基本地图配置的浮动操作视图数组(通常只有“指南针”按钮)。由于 Google 可能会在未来的 SDK 版本中添加按钮或重新排列按钮顺序,因此请避免依赖固定的数组索引。而是应遍历数组,通过将其 type 属性与 NavigationButtonKnownType.compass 进行比较来读取每个按钮的类型,并通过调用其 view 属性来提取物理视图。请注意,此数组中的确切按钮与主动导航期间可用的按钮不同。

个有效的指导组件

当精细导航开始时,SDK 会解锁整套导航控件。您的 ActiveGuidanceUIState 对象提供对以下 Google 组件的访问权限:

  • turnCard:主标题横幅,显示即将到来的机动方向、距离测量和车道指引。将其放置在布局顶部,以建立熟悉的导航层次结构,并围绕它锚定自定义视图。

  • etaCard:页脚横幅,显示预计到达时间、剩余行程时间和到目的地的剩余距离。您可以将其放置在屏幕底部边缘,也可以将其坐标与自定义任务管理工作表集成。

  • tripProgressBar:一个垂直进度条,用于向驾驶员显示其在当前路线上已行驶的距离。与旧版布局(严格固定在地图边缘)不同,您可以完全自由地将其锚定在任何位置,例如,沿着自定义容器的侧边。

  • speedWidget:用于显示车速表和限速标志的浮动控件。根据您的 API 设置(GMSMapView 的 shouldDisplaySpeedometer 和 shouldDisplaySpeedLimit 属性)和数据可用性,此视图会在运行时在四种视觉状态之间动态调整大小(不显示任何内容、仅显示当前速度、仅显示限速或同时显示这两个读数)。由于微件可能会在这些尺寸之间切换,且不会发出通知,因此请始终使用约束条件锚定周围的视图,以便布局自动调整,防止空间重叠。

  • buttons:一个展开的浮动操作视图数组,用于显示处于活动状态的导航状态(通常包括指南针和异常路况报告按钮)。与在导航就绪状态下一样,您可以通过按 type(.compass 或 .reporting)过滤数组并使用其 view 属性提取物理视图,来查找和提取各个按钮视图。 然后,您可以单独放置这些视图,也可以将它们添加到 GMSNavigationHidingStackView 中。此堆栈视图会垂直排列按钮,并在空间不足时从其 arrangedSubviews 数组的末尾开始隐藏按钮。请勿在 管理的视图上设置 isHidden。
提示:摄像头取景的最佳实践。转向卡片的视图具有基于 NavigationStyleValues.headerNominalHeight 的固定高度,该高度足以容纳任何转向卡片内容。为了保持相机取景稳定,请将 viewport 的顶部直接约束到 turnCard 的底部边缘,如实现示例中所示。

动态提示组件

在主动引导期间,系统会独立触发提示,例如突发事件提醒或测速摄像头警告。

当提示准备好显示时,SDK 会调用委托的 showPrompt(navMap:newPrompt:) 回调,并直接传递 newPrompt 视图。您的委托负责将此提示顺利放置在布局上方(通常锚定到地图容器的底部边缘)。

由于传入的提示会覆盖屏幕的下半部分,因此您必须更新布局,以防止提示与视口、Google 徽标或任何底部对齐的按钮重叠。

您的委托对象还可控制提示进入和退出屏幕的方式。如需了解详情,请参阅自定义提示。

处理屏幕尺寸和宽屏模式

为了处理地图尺寸和屏幕方向的变化,布局委托使用了以下功能:

  • 视图大小调整:每当地图的物理尺寸发生变化时,都会调整布局。

  • 宽屏模式:当地图足够宽时,切换到宽屏格式布局变体。

响应视图大小调整

分屏布局、容器滑块和设备旋转通常会触发 sizeChanged(navMap:newState:) 回调。使用此回调可对自定义界面进行常规自适应调整。实现 sizeChanged(navMap:newState:),以便针对新的屏幕宽高比重新定位自定义元素,应用您自己的自定义宽度或高度断点,并检测大小调整事件何时切换宽屏模式,从而安全地切换布局变体。

了解宽屏模式

当地图容器足够宽,可以并排显示界面组件时,宽屏模式就会激活。

从状态对象中读取 wideMode 布尔值,以重新定位自定义界面元素,并让地图中心保持清晰,以便司机查看。由于 Google 提供的组件(例如转向卡片和预计到达时间卡片)在宽屏模式下会自动缩小和调整形状,因此读取此布尔值可确保您的布局在 Google 组件更新的确切时刻进行调整。

请考虑以下示例,了解如何在标准模式和宽屏模式下定位布局组件:

  • 标准纵向模式:将转向卡片放在屏幕顶部,将预计到达时间卡片放在屏幕底部。

  • 宽屏模式:将转向卡片和预计到达时间卡片沿屏幕前缘放置,转向卡片位于顶部,预计到达时间卡片位于底部。将任一卡片放置在远离前缘的位置会导致未定义的行为。您可以将视口和您自己的内容放置在任何位置;常见的选择是卡片的尾端。

在状态转换期间检查宽屏模式

如果您的布局支持宽模式变体,请在每个状态转换回调中评估 wideMode 布尔值,而不是仅依赖于 sizeChanged(navMap:newState:)。

在 enableNavigationUI(navMap:newState:) 和 startGuidance(navMap:newState:oldState:) 等回调期间检查状态对象,以处理横屏模式下的初始应用启动。此方法可确保布局在新的导航状态开始时评估宽屏模式状态并激活正确的标准或宽屏布局变体。

与 Google 样式保持一致

为了帮助您的自定义视图与 Google 提供的组件的间距保持一致,SDK 提供了 NavigationStyleValues 类。其类属性会返回以点为单位的测量值。

例如,如需将自定义按钮放置在屏幕顶部与转向卡片相对的角落,请使用 NavigationStyleValues.headerTopPadding 作为按钮的上外边距。然后,该按钮会与转向卡片的上边缘对齐。

可用的测量值如下:

  • NavigationStyleValues.buttonHorizontalPadding
  • NavigationStyleValues.buttonVerticalPadding
  • NavigationStyleValues.defaultCornerRadius
  • NavigationStyleValues.elementHorizontalPadding
  • NavigationStyleValues.headerFooterHorizontalPadding
  • NavigationStyleValues.headerNominalHeight
  • NavigationStyleValues.headerTopPadding

Google 提供的视图还包含内置外边距。当您堆叠两个 Google 提供的视图时,请将其中一个视图限制为另一个视图的 layoutMarginsGuide,这样它们的边距就不会叠加。例如,如需将速度 widget 放置在预计到达时间卡片上方,请将 speedWidget 的底部锚点限制为 etaCard.layoutMarginsGuide.topAnchor。

自定义提示

布局委托可控制提示进入和退出屏幕的方式,以及提示的哪些角是圆角。

动画提示

SDK 不会为您添加提示动画。您的委托对象决定每个提示的显示和消失方式:

  • 在 showPrompt(navMap:newPrompt:) 中,将提示添加到地图视图的起始位置,然后通过动画将其移动到目标位置。例如,在可见区域正下方或底部动作条后面启动提示,并使其上边缘与动作条顶部对齐。
  • 在 hidePrompt(navMap:oldPrompt:) 中,以动画形式将提示移出视图,然后在动画结束时将其从父视图中移除。
  • 在 changePrompt(navMap:newPrompt:oldPrompt:) 中,同时运行隐藏和显示动画,以将一个提示替换为另一个提示。

实现示例使用了此方法。

如需关闭有关路况、更优路线和突发事件的提示,请将 GMSNavigator 的 shouldDisplayPrompts 属性设置为 false。

圆角提示

提示符合 NavigationRoundedCornerView 协议,因此您可以选择哪些角采用圆角。默认情况下,提示仅圆角化其顶部角落,这适合锚定到屏幕底部边缘的提示。如果您将提示放置在其他内容(例如底部动作条)上方,请将所有四个角都设为圆角:

newPrompt.roundedCorners = [
  .layerMinXMinYCorner, .layerMaxXMinYCorner,
  .layerMinXMaxYCorner, .layerMaxXMaxYCorner,
]

示例:基于约束条件的布局实现

以下示例展示了一个基本布局委托,该委托使用自动布局约束条件来定位转向卡片、预计到达时间卡片、按钮、Google 徽标和视口。它还会在屏幕上滑入和滑出提示。为使示例简短,它不处理宽屏模式、速度微件或行程进度条,因此它仅适用于支持纵向屏幕方向的应用。

import GoogleNavigation
import UIKit

/// A layout delegate that positions the essential navigation UI elements: the
/// turn card, ETA card, buttons, Google logo, and viewport. It also slides
/// prompts on and off the screen.
@MainActor
final class BasicLayoutDelegate: NSObject, NavigationLayoutDelegate {
  /// How long prompts take to slide on or off the screen, in seconds.
  private static let promptAnimationDuration: TimeInterval = 0.4

  /// The views and constraints for one navigation state.
  @MainActor
  private final class StateElements {
    let buttonStack = GMSNavigationHidingStackView(
      position: .bottom, alignment: .trailing)
    var constraints: [NSLayoutConstraint] = []
    private unowned let navMap: GMSMapView
    private let buttons: [UIView]
    private let topLevelViews: [UIView]

    init(
      navMap: GMSMapView, buttons: [NavigationUIButton],
      topLevelViews: [UIView]
    ) {
      self.navMap = navMap
      self.buttons = buttons.map { $0.view }
      self.topLevelViews = topLevelViews
    }

    /// Adds the views to the map view, and then activates the constraints.
    func activate() {
      topLevelViews.forEach { navMap.addSubview($0) }
      buttonStack.arrangedSubviews = buttons
      navMap.addSubview(buttonStack)
      NSLayoutConstraint.activate(constraints)
    }

    /// Deactivates the constraints, and then removes the views.
    func deactivate() {
      NSLayoutConstraint.deactivate(constraints)
      buttonStack.removeFromSuperview()
      buttonStack.arrangedSubviews = []
      topLevelViews.forEach { $0.removeFromSuperview() }
    }
  }

  private var navigationReadyElements: StateElements?
  private var activeGuidanceElements: StateElements?
  private var currentPrompt: NavigationPromptView?
  private var promptBottomConstraint: NSLayoutConstraint?

  /// Constraints that apply in both the navigation-ready and active guidance
  /// states.
  private var sharedConstraints: [NSLayoutConstraint] = []

  /// Marks the bottom of the map content area. The guide stays above the ETA
  /// card and above any prompt that's on the screen.
  private let mapContentsBottomGuide = UILayoutGuide()

  // MARK: - Navigation-ready state

  func enableNavigationUI(
    navMap: GMSMapView, newState: NavigationReadyUIState
  ) {
    let safeArea = navMap.safeAreaLayoutGuide
    let bottomGuide = mapContentsBottomGuide
    navMap.addLayoutGuide(bottomGuide)

    // Keep the guide at the bottom of the safe area unless a higher-priority
    // constraint pushes it up.
    let lowPriorityBottom = bottomGuide.bottomAnchor.constraint(
      equalTo: safeArea.bottomAnchor)
    lowPriorityBottom.priority = .defaultLow
    sharedConstraints = [
      bottomGuide.heightAnchor.constraint(equalToConstant: 0),
      lowPriorityBottom,
    ]

    let logo = newState.googleLogo
    let viewport = newState.viewport
    let elements = StateElements(
      navMap: navMap, buttons: newState.buttons,
      topLevelViews: [logo, viewport])
    let buttonStack = elements.buttonStack
    elements.constraints = sharedConstraints + [
      buttonStack.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      buttonStack.topAnchor.constraint(equalTo: safeArea.topAnchor),
      buttonStack.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      logo.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      logo.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      viewport.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      viewport.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      viewport.topAnchor.constraint(equalTo: safeArea.topAnchor),
      viewport.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),
    ]
    elements.activate()
    navigationReadyElements = elements
  }

  func disableNavigationUI(
    navMap: GMSMapView, oldState: NavigationReadyUIState
  ) {
    currentPrompt?.removeFromSuperview()
    currentPrompt = nil
    promptBottomConstraint = nil
    navigationReadyElements?.deactivate()
    navigationReadyElements = nil
    navMap.removeLayoutGuide(mapContentsBottomGuide)
  }

  // MARK: - Active guidance state

  func startGuidance(
    navMap: GMSMapView, newState: ActiveGuidanceUIState,
    oldState: NavigationReadyUIState
  ) {
    navigationReadyElements?.deactivate()

    let safeArea = navMap.safeAreaLayoutGuide
    let bottomGuide = mapContentsBottomGuide
    let turnCard = newState.turnCard
    let etaCard = newState.etaCard
    let logo = newState.googleLogo
    let viewport = newState.viewport
    let elements = StateElements(
      navMap: navMap, buttons: newState.buttons,
      topLevelViews: [turnCard, etaCard, viewport, logo])
    let buttonStack = elements.buttonStack
    elements.constraints = sharedConstraints + [
      turnCard.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      turnCard.topAnchor.constraint(equalTo: navMap.topAnchor),

      etaCard.leadingAnchor.constraint(equalTo: navMap.leadingAnchor),
      etaCard.bottomAnchor.constraint(equalTo: navMap.bottomAnchor),
      bottomGuide.bottomAnchor.constraint(
        lessThanOrEqualTo: etaCard.topAnchor),

      buttonStack.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      buttonStack.topAnchor.constraint(equalTo: turnCard.bottomAnchor),
      buttonStack.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      logo.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      logo.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),

      // Frame the camera in the open area below the turn card.
      viewport.leadingAnchor.constraint(equalTo: safeArea.leadingAnchor),
      viewport.trailingAnchor.constraint(equalTo: safeArea.trailingAnchor),
      viewport.topAnchor.constraint(equalTo: turnCard.bottomAnchor),
      viewport.bottomAnchor.constraint(equalTo: bottomGuide.topAnchor),
    ]
    elements.activate()
    activeGuidanceElements = elements
  }

  func stopGuidance(
    navMap: GMSMapView, newState: NavigationReadyUIState,
    oldState: ActiveGuidanceUIState
  ) {
    activeGuidanceElements?.deactivate()
    activeGuidanceElements = nil
    navigationReadyElements?.activate()
  }

  // MARK: - Prompts

  func showPrompt(navMap: GMSMapView, newPrompt: NavigationPromptView) {
    navMap.addSubview(newPrompt)
    currentPrompt = newPrompt

    // Start with the prompt just below the bottom edge of the map view.
    let bottomConstraint = newPrompt.bottomAnchor.constraint(
      equalTo: navMap.bottomAnchor, constant: newPrompt.bounds.height)
    promptBottomConstraint = bottomConstraint
    NSLayoutConstraint.activate([
      bottomConstraint,
      newPrompt.leadingAnchor.constraint(
        equalTo: navMap.safeAreaLayoutGuide.leadingAnchor),
      // Keep the map content above the prompt.
      mapContentsBottomGuide.bottomAnchor.constraint(
        lessThanOrEqualTo: newPrompt.topAnchor),
    ])

    // Lay out the prompt in its starting position, and then slide it up.
    navMap.layoutIfNeeded()
    UIView.animate(withDuration: Self.promptAnimationDuration) {
      bottomConstraint.constant = 0
      navMap.layoutIfNeeded()
    }
  }

  func hidePrompt(navMap: GMSMapView, oldPrompt: NavigationPromptView) {
    guard let bottomConstraint = promptBottomConstraint else { return }
    promptBottomConstraint = nil
    currentPrompt = nil

    // Slide the prompt down, and then remove it.
    UIView.animate(withDuration: Self.promptAnimationDuration) {
      bottomConstraint.constant = oldPrompt.bounds.height
      navMap.layoutIfNeeded()
    } completion: { _ in
      oldPrompt.removeFromSuperview()
    }
  }

  func changePrompt(
    navMap: GMSMapView, newPrompt: NavigationPromptView,
    oldPrompt: NavigationPromptView
  ) {
    // Slide the old prompt out and the new prompt in at the same time.
    hidePrompt(navMap: navMap, oldPrompt: oldPrompt)
    showPrompt(navMap: navMap, newPrompt: newPrompt)
  }
}

如需使用委托,请在启用导航之前在地图视图中设置委托,如基本核对清单中所述。

限制和已知问题

布局自定义不适用于 CarPlay,因为 CarPlay 使用固定布局模板。如果您在为 CarPlay 屏幕创建的地图视图上调用 setLayoutDelegate(_:),该方法会抛出错误。

对于影响特定 SDK 版本的问题,请参阅版本说明中的已知问题。