自訂導覽版面配置

動畫範例:iOS 上的自訂導覽版面配置

注意:本文使用 Swift 命名慣例來命名類別、通訊協定和 API。

使用版面配置自訂功能,控制 Google 提供的檢視畫面和自訂業務檢視畫面的檢視區塊階層和畫面位置。

請實作版面配置委派代表來管理整個畫面的版面配置,而不是將自訂檢視畫面新增至預先定義的版面配置版位。在導航狀態轉換期間,版面配置委派代表會收到 Google 提供的元件,例如轉彎資訊卡、預計到達時間資訊卡和按鈕。請使用標準 iOS 自動版面配置限制,將這些元素與自訂內容並排。

使用這個架構,即可在畫面上精確定位自訂業務資訊 (例如訂單狀態或取貨操作說明),同時避免檢視畫面重疊。

版面配置自訂功能的運作方式

版面配置自訂功能採用委派設計模式。SDK 不會自動在畫面上繪製或定位 UI 元件,而是直接將元件傳遞至您編寫的自訂類別:版面配置委派。

如要自訂版面配置,請建立符合 NavigationLayoutDelegate 通訊協定的類別,並將例項傳遞至 GMSMapView 的 setLayoutDelegate(_:) 方法。每當導航狀態轉換時 (例如從基本地圖切換至即時路線指引),SDK 就會對委派物件執行回呼方法,並提供該特定狀態可用的 UI 元件。

為提供流暢、整合的使用者體驗,這個架構明確區分了應用程式和 SDK 的責任。使用版面配置委派來自訂下列項目:

  • 建構檢視區塊階層:選擇要為每個 UI 狀態新增至畫面的 Google 元件和自訂業務檢視區塊。
  • 定位每個元素:設定精確的螢幕錨點、邊界和版面配置位置。請避免對 Google 元件套用自訂寬度或高度限制,因為 Google 元件會自行計算內部尺寸。
  • 設定基本地圖的框架:使用可視區域元件的座標,定義地圖攝影機的可見邊界。
  • 分層顯示畫面:決定自訂檢視畫面要浮動顯示在 Google 內建控制項的上方、下方或旁邊。

同時,您無法使用版面配置委派來自訂下列 Google 元件:

  • 元件尺寸:Google 提供的元件大小和內部尺寸,由 SDK 自動計算。
  • 觸發條件:根據即時路線資料,顯示動態快訊或提示。

導入原則

編寫版面配置委派時,請遵守下列規則,避免版面配置錯誤或執行階段當機:

  • 預設為空白畫面:只有在委派明確將 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 提供的檢視畫面。

停用導航 UI 時,GMSUISettings 的 compassButton 屬性仍會控制指南針。啟用導航功能後,委派代表會決定是否要顯示 buttons 陣列中的指南針。

基本檢查清單

請按照下列基本步驟和規定,成功導入版面配置委派:

  • 在啟用導航前設定委派:請先在 GMSMapView 上呼叫 setLayoutDelegate(_:),再使用 isNavigationEnabled 或 enableNavigation(with:) 啟用導航。如果導航功能已啟用,或您為 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 可能會忽略這些舊版通話。

UI 狀態和 Google 元件

導航狀態轉換時,SDK 會將唯讀的 NavigationUIState 物件傳遞至委派回呼。這個物件會將目前的版面配置設定旗標,與轉譯畫面所需的 Google 元件 (例如 UIView 執行個體) 組合在一起。

版面配置委派代表會管理四種作業狀態的檢視區塊階層。下列狀態圖說明 SDK 如何在導航狀態之間轉換,以及在委派物件上執行的回呼方法:

NavigationLayoutDelegate 生命週期狀態轉換圖,顯示「非導航」、「導航就緒」和「主動導引」狀態之間的轉換。

提供給委派代表的特定 Google 元件,取決於導航生命週期的目前階段。

必要元件 (所有導覽狀態)

無論目前的導航階段為何,您都必須在檢視區塊階層中加入、定位及保留下列 Google 元件:

  • Google 標誌 (googleLogo):這個元件會顯示必要的 Google 地圖標誌。如果啟用「重新置中」按鈕,當駕駛人捲動地圖,使車輛不在畫面中央時,標誌會自動轉換為這個按鈕。因此,標誌的位置會決定「重新置中」按鈕顯示的位置。建議將標誌放在版面配置的左下角,符合標準地圖 UI 的預期。

  • 可視區域 (viewport): 定義攝影機取景邊界的隱形 UIView。SDK 會根據可視區域的位置,將車輛 V 形符號置中,並繪製有效路線。調整可視區域的位置,使其覆蓋螢幕上未遮蔽的開放區域,安全避開自訂底部功能表等不透明疊加層。

在導航就緒狀態下,UI 會保持精簡,讓使用者專注於基本地圖。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:) 回呼。您可以使用這個回呼,對自訂 UI 進行一般回應式調整。實作 sizeChanged(navMap:newState:),根據新的螢幕顯示比例重新定位自訂元素、套用自訂寬度或高度中斷點,以及偵測調整大小事件何時切換寬螢幕模式,以便安全地切換版面配置變化版本。

瞭解寬版顯示模式

如果地圖容器寬度足以並排顯示 UI 元件,就會啟用寬版模式。

從狀態物件讀取 wideMode 布林值,重新定位自訂 UI 元素,並確保地圖中心不會遮擋駕駛視線。由於 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,以免邊界累加。舉例來說,如要將速度小工具放在預計到達時間資訊卡上方,請將 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 畫面建立的地圖檢視上呼叫 setLayoutDelegate(_:),該方法會擲回錯誤。

如要瞭解影響特定 SDK 版本的問題,請參閱版本資訊中的「已知問題」。