
レイアウトのカスタマイズを使用すると、Google が提供するビューと独自のカスタム ビジネスビューの両方で、ビュー階層と画面配置を制御できます。
事前定義されたレイアウト スロットにカスタムビューを追加する代わりに、レイアウト デリゲートを実装して画面全体のレイアウトを管理します。ナビゲーションの状態遷移中に、レイアウト デリゲートは Google が提供するコンポーネント(ターンカード、到着予定時刻カード、ボタンなど)を受け取ります。これらの要素は、標準の iOS Auto Layout 制約を使用して、独自のカスタム コンテンツと一緒に配置します。
このフレームワークを使用すると、注文ステータスや受け取り手順などのカスタム ビジネス情報を画面上に正確に配置し、ビューが重ならないようにすることができます。
レイアウトのカスタマイズの仕組み
レイアウトのカスタマイズでは、委任デザイン パターンを使用します。SDK は、画面上で UI コンポーネントを自動的に描画または配置するのではなく、作成したカスタムクラス(レイアウト デリゲート)に直接渡します。
レイアウトをカスタマイズするには、NavigationLayoutDelegate プロトコルに準拠するクラスを作成し、GMSMapView の setLayoutDelegate(_:) メソッドにインスタンスを渡します。ナビゲーションの状態が遷移するたびに(基本地図からアクティブなターンバイターン方式のルート案内への移行など)、SDK はデリゲートのコールバック メソッドを実行し、その特定の状態に使用できる UI コンポーネントを提供します。シームレスで統合されたユーザー エクスペリエンスを提供するため、このフレームワークでは、アプリと SDK の間で責任を明確に分離しています。レイアウト デリゲートを使用して、以下をカスタマイズします。
- ビュー階層を構築する: 各 UI 状態の画面に追加する Google コンポーネントとカスタム ビジネスビューを正確に選択します。
- すべての要素を配置する: 画面のアンカー、マージン、レイアウトの配置を正確に設定します。Google コンポーネントは独自の内部寸法を計算するため、Google コンポーネントにカスタムの幅や高さの制約を適用しないでください。
- 基本地図をフレームに収める: ビューポート コンポーネントの座標を使用して、地図カメラの表示境界を定義します。
- 画面を重ねる: カスタムビューを Google の組み込みコントロールの上、下、または横に配置するかどうかを決定します。
一方、次の Google コンポーネントは、レイアウト デリゲートでカスタマイズできません。
- コンポーネントのサイズ: Google 提供のコンポーネントのサイズと内部サイズ。SDK によって自動的に計算されます。
- トリガー条件: リアルタイムのルートデータに基づいて動的なアラートやプロンプトが表示されるタイミング。
実装の原則
レイアウト デリゲートを作成する際は、レイアウトのバグやランタイム クラッシュを防ぐために、次のルールに留意してください。
- デフォルトは空の画面です。Google 提供のコンポーネントは、デリゲートが明示的にビュー階層に追加して配置した場合にのみ表示されます。
- レガシー API はサポートされていません。従来の、スロットベースのレイアウト モデルを制御するプロパティとメソッドはサポートされません。カスタム デリゲートが有効になっている場合、想定どおりに動作しない可能性があります。
- 内部ビュー構造を変更しない: Google 提供のコンポーネント(ターンカードや ETA カードなど)のビュー階層を走査または変更しないでください。これらの内部ビュー階層は、基盤となる実装の詳細であるため、SDK のリリースごとに変更される可能性があります。これらを変更すると、今後の SDK の更新時にレイアウトが破損する可能性があります。
以前の API の互換性
カスタム レイアウト デリゲートを使用する際に信頼性の高いレイアウト動作を実現するには、非推奨となる予定の次のレガシー スロットベースの API を使用しないでください。既存のアプリを移行するには、これらの API の使用をカスタム レイアウト デリゲートのコードに置き換えます。
以前のスロットベースの API を表示する
| 以前の API | Layout Delegate の置き換え |
|---|---|
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(_:) |
showPrompt(navMap:newPrompt:) などの NavigationLayoutDelegate プロンプト コールバックに依存します。 |
カスタム レイアウト デリゲートが設定されている間は、以前のレイアウト ガイド(navigationHeaderLayoutGuide や bottomTrailingButtonsLayoutGuide など)とヘッダー アクセサリー ビューのメソッドは機能しません。代わりに、Google 提供のビューにビューを固定します。
ナビゲーション UI が無効になっている間も、GMSUISettings の compassButton プロパティはコンパスを制御します。ナビゲーションが有効になっている間、デリゲートは buttons 配列からコンパスを表示するかどうかを決定します。
Essentials のチェックリスト
レイアウト デリゲートを正常に実装するには、次の重要な手順と要件に従ってください。
-
ナビゲーションを有効にする前にデリゲートを設定する:
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:)で、追加したすべてのビューとレイアウト ガイド(画面上のプロンプトを含む)を削除します。これにより、ある状態のビューが次の状態のビューと重なるのを防ぐことができます。 -
thought ユーザー補助と右から左への記述のサポートを独自に実装する: Google が提供するビューはユーザー補助の基準を満たしていますが、レイアウト全体のユーザー補助については、デリゲートが責任を負います。右から左に記述する言語をサポートするには、左アンカーと右アンカーではなく、先頭アンカーと末尾アンカーを使用します。
-
以前のレイアウト API を避ける: カスタム デリゲートがアクティブな間は、
GMSUISettingsで以前のレイアウト変更プロパティを使用したり、GMSMapViewで以前のレイアウト ガイドを使用したりしないでください。カスタム デリゲートがアタッチされている場合、SDK はこれらの以前の通話機能を無視することがあります。
UI の状態と Google コンポーネント
ナビゲーション状態の遷移が発生すると、SDK は読み取り専用の NavigationUIState オブジェクトをデリゲート コールバックに渡します。このオブジェクトは、現在のレイアウト構成フラグと、画面のレンダリングに必要な Google コンポーネント(UIView インスタンスなど)をバンドルします。
レイアウト デリゲートは、4 つのオペレーション状態にわたってビュー階層を管理します。次の状態図は、SDK がナビゲーション状態間をどのように遷移するか、およびデリゲートで実行するコールバック メソッドを示しています。
デリゲートに提供される特定の Google コンポーネントは、ナビゲーション ライフサイクルの現在のフェーズによって異なります。
必須コンポーネント(すべてのナビゲーション状態)
現在のナビゲーション フェーズに関係なく、次の Google コンポーネントをビュー階層に含めて配置し、表示したままにする必要があります。
Google ロゴ(
googleLogo): このコンポーネントは、必須の Google マップロゴを表示します。[中央に再配置] ボタンが有効になっている場合、ドライバーが地図をスクロールして車両から離れると、ロゴが自動的にこのボタンに変わります。そのため、ロゴの配置によって、[中心を再設定] ボタンが表示される場所が決まります。標準の地図 UI の想定に合わせて、レイアウトの左下隅にロゴを配置することをおすすめします。ビューポート(
viewport): カメラのフレーミング境界を定義する、非表示のUIView。ビューポートの配置によって、SDK が車両のシェブロンを中央に配置し、アクティブなルートラインを描画する場所が決まります。ビューポートを、画面の開いている遮るもののない領域を覆うように配置し、カスタム ボトムシートなどの不透明なオーバーレイを安全に回避します。
ナビゲーション対応コンポーネント
ナビゲーション準備完了状態では、基本地図に集中できるように UI は最小限に抑えられます。NavigationReadyUIState オブジェクトを使用すると、次のものにアクセスできます。
- buttons: 基本地図用に構成されたフローティング アクション ビューの配列(通常はコンパス ボタンのみ)。Google は今後の SDK リリースでボタンを追加したり、ボタンの順序を変更したりする可能性があるため、固定の配列インデックスに依存しないようにしてください。代わりに、配列を反復処理し、
typeプロパティをNavigationButtonKnownType.compassと比較して各ボタンのタイプを読み取り、viewプロパティを呼び出して物理ビューを抽出します。この配列のボタンは、アクティブなガイダンス中に使用できるボタンとは異なります。
アクティブ ガイダンス コンポーネント
ターンバイターン方式のルート案内が開始されると、SDK はナビゲーション コントロールの全機能をロック解除します。ActiveGuidanceUIState オブジェクトを使用すると、次の Google コンポーネントにアクセスできます。
turnCard: 次の 操作の方向、距離の測定、車線案内を表示するメインのヘッダー バナー。レイアウトの上部に配置して、使い慣れたナビゲーション階層を確立し、その周囲にカスタムビューを固定します。etaCard: 到着予定時刻、残りの移動時間、目的地までの残りの距離を表示するフッター バナー。画面の下端に沿って配置したり、座標をカスタムのタスク管理シートに統合したりできます。tripProgressBar: 縦型の進行状況バー。現在のルートに沿ってドライバーが移動した距離を示します。このコントロールを地図の端に厳密に固定していた従来のレイアウトとは異なり、カスタム コンテナの側端など、どこにでも固定できます。speedWidget: 速度計と制限速度の表示を制御するフローティング コントロール。API の設定(GMSMapViewのshouldDisplaySpeedometerプロパティとshouldDisplaySpeedLimitプロパティ)とデータの可用性に応じて、このビューは実行時に 4 つの表示状態(何も表示しない、現在の速度のみ、速度制限のみ、両方の読み取り値)の間で動的にサイズ変更されます。ウィジェットは、これらのサイズ間で予告なく切り替わることがあるため、常に制約を使用して周囲のビューをアンカーし、レイアウトが自動的に適応して空間的な重複を防ぐようにします。
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 が提供するコンポーネント(ターンカードや ETA カードなど)は、ワイドモードでは自動的に縮小して形状が変更されるため、このブール値を読み取ることで、Google コンポーネントが更新されるのとまったく同じタイミングでレイアウトが適応されるようになります。
標準モードとワイドモードでレイアウト コンポーネントを配置する例を次に示します。
標準のポートレート モード: 画面の上部にターンカード、下部に ETA カードを配置します。
ワイドモード: ターンカードと ETA カードを画面の先頭に沿って配置します。ターンカードを上部に、ETA カードを下部に配置します。いずれかのカードを先端から離して配置すると、未定義の動作が発生します。ビューポートと独自のコンテンツはどこにでも配置できます。一般的な選択肢は、カードの後端です。
状態遷移中のワイドモードの確認
レイアウトがワイドモードのバリエーションをサポートしている場合は、sizeChanged(navMap:newState:) のみに依存するのではなく、すべての状態遷移コールバック内で wideMode ブール値を評価します。
enableNavigationUI(navMap:newState:) や startGuidance(navMap:newState:oldState:) などのコールバック中に状態オブジェクトをチェックして、横向きでのアプリの初回起動を処理します。このアプローチにより、レイアウトがワイドモードの状態を評価し、新しいナビゲーション状態が開始された瞬間に、正しい標準レイアウト バリアントまたはワイド レイアウト バリアントが有効になります。
Google のスタイルに合わせる
カスタムビューの間隔を Google 提供のコンポーネントの間隔に合わせるために、SDK には NavigationStyleValues クラスが用意されています。クラス プロパティは、ポイント単位で測定値を返します。
たとえば、ターンカードの反対側の画面上部の隅にカスタムボタンを配置するには、ボタンの上マージンとして NavigationStyleValues.headerTopPadding を使用します。ボタンは、ターンカードの上端に揃えられます。
次の測定値を使用できます。
NavigationStyleValues.buttonHorizontalPaddingNavigationStyleValues.buttonVerticalPaddingNavigationStyleValues.defaultCornerRadiusNavigationStyleValues.elementHorizontalPaddingNavigationStyleValues.headerFooterHorizontalPaddingNavigationStyleValues.headerNominalHeightNavigationStyleValues.headerTopPadding
Google 提供のビューには、組み込みのマージンも含まれています。Google が提供する 2 つのビューを重ねる場合は、マージンが加算されないように、一方を他方の layoutMarginsGuide に制約します。たとえば、速度ウィジェットを ETA カードの上に配置するには、speedWidget の下部アンカーを etaCard.layoutMarginsGuide.topAnchor に制約します。
プロンプトのカスタマイズ
レイアウト デリゲートは、プロンプトが画面に表示される方法と画面から消える方法、および角を丸くするプロンプトを制御します。
プロンプトのアニメーション化
SDK はプロンプトをアニメーション化しません。デリゲートは、各プロンプトの表示と非表示の方法を決定します。
showPrompt(navMap:newPrompt:)で、開始位置のマップビューにプロンプトを追加し、アニメーションで所定の位置に移動します。たとえば、表示領域のすぐ下、または上端がシートの上端に揃えられたボトムシートの背後でプロンプトを開始します。hidePrompt(navMap:oldPrompt:)で、プロンプトをビューの外に移動するアニメーションを再生し、アニメーションが終了したら、プロンプトをスーパービューから削除します。changePrompt(navMap:newPrompt:oldPrompt:)で、非表示アニメーションと表示アニメーションを同時に実行して、プロンプトを別のプロンプトに置き換えます。
実装例では、このアプローチを使用しています。
交通状況、より良いルート、インシデントに関するプロンプトをオフにするには、GMSNavigator の shouldDisplayPrompts プロパティを false に設定します。
プロンプトの角を丸くする
プロンプトは NavigationRoundedCornerView プロトコルに準拠しているため、角を丸くするかどうかを選択できます。デフォルトでは、プロンプトの上部の角のみが丸められます。これは、画面の下端に固定されたプロンプトに適しています。ボトムシートなど、他のコンテンツの上にプロンプトを配置する場合は、4 つの角をすべて丸くします。
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 バージョンに影響する問題については、リリースノートの既知の問題をご覧ください。