Personalizzare il layout di navigazione

Animazione di esempio che mostra un layout di navigazione personalizzato su iOS

Nota: questo documento utilizza le convenzioni di denominazione di Swift per classi, protocolli e API.

Utilizza la personalizzazione del layout per controllare la gerarchia di oggetti View e il posizionamento sullo schermo sia per le visualizzazioni fornite da Google sia per le tue visualizzazioni aziendali personalizzate.

Anziché aggiungere visualizzazioni personalizzate in slot di layout predefiniti, implementa un delegato di layout per gestire il layout dell'intero schermo. Durante le transizioni di stato della navigazione, il delegato del layout riceve i componenti forniti da Google, come la scheda di svolta, la scheda ETA e i pulsanti. Posiziona questi elementi accanto ai tuoi contenuti personalizzati utilizzando i vincoli di layout automatico standard di iOS.

Utilizza questo framework per posizionare con precisione sullo schermo informazioni aziendali personalizzate, come lo stato dell'ordine o le istruzioni per il ritiro, evitando al contempo la sovrapposizione delle visualizzazioni.

Come funziona la personalizzazione del layout

La personalizzazione del layout utilizza un pattern di progettazione della delega. Anziché disegnare o posizionare automaticamente i componenti dell'interfaccia utente sullo schermo, l'SDK li passa direttamente a una classe personalizzata che scrivi: il delegato del layout.

Per personalizzare il layout, crea una classe conforme al protocollo NavigationLayoutDelegate e passa un'istanza al metodo setLayoutDelegate(_:) di GMSMapView. Ogni volta che lo stato di navigazione cambia, ad esempio quando si passa dalla mappa base alle indicazioni stradali passo passo attive, l'SDK esegue i metodi di callback sul tuo delegato e fornisce i componenti della UI disponibili per quello stato specifico.

Per offrire un'esperienza utente fluida e integrata, questo framework stabilisce una chiara separazione delle responsabilità tra la tua app e l'SDK. Utilizza il delegato del layout per personalizzare quanto segue:

  • Crea la gerarchia di oggetti View: scegli esattamente quali componenti Google e visualizzazioni aziendali personalizzate aggiungere alla schermata per ogni stato della UI.
  • Posiziona ogni elemento: imposta ancoraggi, margini e posizionamento del layout precisi sullo schermo. Evita di applicare vincoli di larghezza o altezza personalizzati ai componenti Google, perché questi calcolano le proprie dimensioni interne.
  • Inquadra la mappa base: definisci i confini visibili della fotocamera della mappa utilizzando le coordinate del componente dell'area visibile.
  • Disponi lo schermo a livelli: decidi se le visualizzazioni personalizzate devono essere posizionate sopra, sotto o accanto ai controlli integrati di Google.

Nel frattempo, non puoi personalizzare i seguenti componenti Google con il delegato del layout:

  • Dimensioni dei componenti: le dimensioni e le dimensioni interne dei componenti forniti da Google, calcolate automaticamente dall'SDK.
  • Condizioni di attivazione: quando vengono visualizzati avvisi o prompt dinamici in base ai dati sul percorso in tempo reale.

Principi di implementazione

Quando scrivi un delegato di layout, tieni presente le seguenti regole per evitare bug di layout o arresti anomali in fase di runtime:

  • Per impostazione predefinita, viene visualizzata una schermata vuota: i componenti forniti da Google vengono visualizzati solo se il delegato li aggiunge esplicitamente alla gerarchia di oggetti View e li posiziona.
  • Le API legacy non sono supportate: le proprietà e i metodi che controllano il modello di layout legacy basato su slot non saranno supportati e potrebbero non comportarsi come previsto quando è attivo un delegato personalizzato.
  • Non modificare le strutture di visualizzazione interne: non attraversare o modificare le gerarchie di oggetti View dei componenti forniti da Google (come la scheda di svolta o la scheda ETA). Poiché queste gerarchie di oggetti View interne sono dettagli di implementazione sottostanti, sono soggette a modifiche tra le release dell'SDK. La loro modifica può causare l'interruzione del layout durante i futuri aggiornamenti dell'SDK.

Compatibilità con l'API legacy

Per un comportamento affidabile del layout quando utilizzi un delegato di layout personalizzato, evita di utilizzare le seguenti API basate su slot legacy, che verranno ritirate. Per eseguire la migrazione di un'app esistente, sostituisci l'utilizzo di queste API con il codice nel delegato del layout personalizzato:

Mostra le API legacy basate su slot

API Legacy Sostituzione del delegato del layout
isNavigationHeaderEnabled Leggi la vista turnCard in startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled Leggi la vista etaCard in startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled Leggi la vista tripProgressBar in startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled Individua il pulsante .reporting nell'array buttons.
setHeaderAccessoryView(_:) e invalidateLayout(forAccessoryView:) Aggiungi la tua visualizzazione al layout e ancorala a turnCard.
padding e paddingAdjustmentBehavior Posiziona la visuale di viewport per inquadrare la videocamera.
navigationHeaderLayoutGuide Ancorare le visualizzazioni personalizzate direttamente a turnCard.
navigationFooterLayoutGuide Ancorare le visualizzazioni personalizzate direttamente alla etaCard.
bottomTrailingButtonsLayoutGuide Ancorare i pulsanti direttamente alle guide etaCard o del contenitore.
bottomTrailingButtonsAlternateLayoutGuide Ancora i pulsanti direttamente al pulsante .reporting o ai vincoli wideMode.
navigatorWillPresentPrompt(_:) e navigatorDidDismissPrompt(_:) Affidati ai callback dei prompt di NavigationLayoutDelegate, come showPrompt(navMap:newPrompt:).

Quando è impostato un delegato di layout personalizzato, le guide di layout legacy (come navigationHeaderLayoutGuide o bottomTrailingButtonsLayoutGuide) e i metodi di visualizzazione degli accessori dell'intestazione non hanno alcun effetto. Ancora le tue visualizzazioni a quelle fornite da Google.

La proprietà compassButton di GMSUISettings controlla comunque la bussola mentre la UI di navigazione è disattivata. Mentre la navigazione è attiva, il delegato decide se mostrare la bussola dall'array buttons.

Elenco di controllo di Essentials

Segui questi passaggi e requisiti essenziali per implementare correttamente il delegato del layout:

  • Imposta il delegato prima di attivare la navigazione: chiama setLayoutDelegate(_:) su GMSMapView prima di attivare la navigazione con isNavigationEnabled o enableNavigation(with:). Il metodo genera un errore se la navigazione è già attiva o se hai creato la visualizzazione mappa per uno schermo CarPlay. La visualizzazione mappa mantiene un riferimento forte al tuo delegato. Per ripristinare il layout integrato, passa nil mentre la navigazione è disattivata.

    // 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
        
  • Collega le visualizzazioni obbligatorie: devi aggiungere il logo Google (googleLogo) e l'area visibile (viewport) al contenitore del layout in tutti gli stati di navigazione.

  • Rispetta le dimensioni fisse: l'SDK determina le dimensioni di diversi componenti forniti da Google. Non applicare vincoli di larghezza o altezza personalizzati ai seguenti elementi:

    • Scheda di svolta
    • scheda ETA
    • Prompt in arrivo
    • Logo Google
    • Widget Velocità
  • Aggiungi elementi come visualizzazioni secondarie: aggiungi le visualizzazioni fornite da Google direttamente come visualizzazioni secondarie alla visualizzazione contenitore personalizzata o direttamente a GMSMapView.

  • Allineamento tramite le guide di layout: le visualizzazioni fornite da Google includono margini visivi integrati. Per allineare perfettamente gli elementi personalizzati con i componenti forniti da Google, vincola le visualizzazioni alla proprietà layoutMarginsGuide della visualizzazione Google.

  • Pulizia all'uscita dallo stato: in stopGuidance(navMap:newState:oldState:), rimuovi le visualizzazioni di indicazioni stradali attive e disattiva i relativi vincoli prima di ripristinare il layout pronto per la navigazione. In disableNavigationUI(navMap:oldState:), rimuovi tutte le visualizzazioni e le guide di layout che hai aggiunto, inclusi eventuali prompt sullo schermo. In questo modo, le visualizzazioni di uno stato non si sovrappongono a quelle dello stato successivo.

  • Supporto per l'accessibilità e la lettura da destra a sinistra: le visualizzazioni fornite da Google soddisfano gli standard di accessibilità, ma il tuo delegato è responsabile dell'accessibilità del layout complessivo. Per supportare le lingue con scrittura da destra a sinistra, utilizza gli ancoraggi iniziali e finali anziché quelli sinistri e destri.

  • Evita le API di layout legacy: non utilizzare proprietà di modifica del layout legacy su GMSUISettings o guide di layout legacy su GMSMapView mentre è attivo un delegato personalizzato. L'SDK potrebbe ignorare queste chiamate legacy quando è collegato un delegato personalizzato.

Stati della UI e componenti Google

Quando si verifica una transizione dello stato di navigazione, l'SDK passa un oggetto NavigationUIState di sola lettura al callback del delegato. Questo oggetto raggruppa i flag di configurazione del layout corrente con i componenti Google (ad esempio le istanze UIView) necessari per eseguire il rendering della schermata.

Il delegato del layout gestisce la gerarchia di oggetti View in quattro stati operativi. Il seguente diagramma di stato illustra il modo in cui l'SDK passa da uno stato di navigazione all'altro e i metodi di callback che esegue sul tuo delegato:

Diagramma di transizione dello stato del ciclo di vita di NavigationLayoutDelegate che mostra le transizioni tra gli stati Non-Navigation, Navigation Ready e Active Guidance.

I componenti Google specifici forniti al tuo delegato dipendono dalla fase corrente del ciclo di vita della navigazione.

Componenti obbligatori (tutti gli stati di navigazione)

Indipendentemente dalla fase di navigazione corrente, devi includere, posizionare e mantenere visibili i seguenti componenti Google nella gerarchia di oggetti View:

  • Logo Google (googleLogo): questo componente mostra il logo Google Maps obbligatorio. Se il pulsante Ricentra è attivo, il logo si trasforma automaticamente in questo pulsante quando il conducente scorre la mappa lontano dal veicolo. Di conseguenza, il posizionamento del logo determina dove viene visualizzato il pulsante Ricentra. Ti consigliamo di posizionare il logo nell'angolo in basso a sinistra del layout per soddisfare le aspettative standard dell'interfaccia utente della mappa.

  • Area visibile (viewport): un UIView invisibile che definisce i confini dell'inquadratura della videocamera. La posizione dell'area visibile determina esattamente dove l'SDK centra il simbolo di direzione del veicolo e disegna la linea del percorso attivo. Posiziona l'area visibile in modo che copra l'area aperta e non oscurata dello schermo, evitando in modo sicuro overlay opachi come i riquadri inferiori personalizzati.

Durante lo stato di navigazione pronta, la UI rimane minima per mantenere l'attenzione sulla mappa base. L'oggetto NavigationReadyUIState fornisce l'accesso a quanto segue:

  • buttons: un array di visualizzazioni di azioni floating configurate per la mappa base (in genere solo il pulsante Bussola). Poiché Google potrebbe aggiungere o riordinare i pulsanti nelle future release dell'SDK, evita di fare affidamento su indici di array fissi. Invece, esegui l'iterazione dell'array, leggi il tipo di ogni pulsante confrontando la sua proprietà type con NavigationButtonKnownType.compass ed estrai la visualizzazione fisica chiamando la sua proprietà view. Tieni presente che i pulsanti esatti in questo array sono diversi da quelli disponibili durante le indicazioni stradali attive.

Componenti di guida attiva

Quando iniziano le indicazioni stradali, l'SDK sblocca la suite completa di controlli di navigazione. L'oggetto ActiveGuidanceUIState fornisce l'accesso ai seguenti componenti Google:

  • turnCard: il banner dell'intestazione principale che mostra le indicazioni per la manovra imminente, le misurazioni della distanza e l'assistenza per il cambio di corsia. Posizionala nella parte superiore del layout per stabilire una gerarchia di navigazione familiare e ancorare le visualizzazioni personalizzate intorno.

  • etaCard: il banner nel piè di pagina che mostra l'ora di arrivo stimata, il tempo di percorrenza rimanente e la distanza rimanente dalla destinazione. Posizionalo lungo il bordo inferiore dello schermo o integra le sue coordinate con i tuoi fogli di gestione delle attività personalizzati.

  • tripProgressBar: una barra di avanzamento verticale che mostra al conducente la distanza percorsa lungo il percorso attuale. A differenza dei layout legacy che lo fissavano rigorosamente al bordo della mappa, hai la totale libertà di ancorarlo ovunque, ad esempio lungo un bordo laterale del tuo container personalizzato.

  • speedWidget: un controllo mobile per il tachimetro e i limiti di velocità indicati. A seconda delle impostazioni dell'API (le proprietà shouldDisplaySpeedometer e shouldDisplaySpeedLimit di GMSMapView) e della disponibilità dei dati, questa visualizzazione si ridimensiona dinamicamente tra quattro stati visivi in fase di runtime (non viene visualizzato nulla, solo la velocità attuale, solo il limite di velocità o entrambe le letture). Poiché il widget può passare da una dimensione all'altra senza preavviso, ancora sempre le visualizzazioni circostanti utilizzando i vincoli in modo che il layout si adatti automaticamente per evitare sovrapposizioni spaziali.

  • buttons: un array espanso di visualizzazioni di azioni floating per lo stato di navigazione attiva (in genere include sia la bussola sia il pulsante di segnalazione di un problema). Come nello stato Pronto per la navigazione, puoi individuare ed estrarre le singole visualizzazioni dei pulsanti filtrando l'array per type (.compass o .reporting) ed estraendo la visualizzazione fisica utilizzando la relativa proprietà view. Puoi quindi posizionarli in modo indipendente o aggiungerli a un GMSNavigationHidingStackView. Questa visualizzazione in pila dispone i pulsanti verticalmente e, quando lo spazio si esaurisce, li nasconde a partire dalla fine dell'array arrangedSubviews. Non impostare isHidden sulle visualizzazioni che gestisce.
Suggerimento: best practice per l'inquadratura della videocamera. La visualizzazione della scheda di svolta ha un'altezza fissa basata su NavigationStyleValues.headerNominalHeight, che è sufficiente per qualsiasi contenuto della scheda di svolta. Per mantenere stabile l'inquadratura della fotocamera, vincola la parte superiore di viewport direttamente al bordo inferiore di turnCard, come mostrato nell'implementazione di esempio.

Componenti dei prompt dinamici

I prompt, come gli avvisi di incidenti o di autovelox, vengono attivati indipendentemente durante la navigazione attiva.

Quando un prompt è pronto per essere visualizzato, l'SDK chiama il callback showPrompt(navMap:newPrompt:) del tuo delegato e passa direttamente una visualizzazione newPrompt. Il delegato è responsabile del posizionamento corretto di questo prompt sul layout (in genere ancorato al bordo inferiore del contenitore della mappa).

Poiché la richiesta in arrivo copre la parte inferiore dello schermo, devi aggiornare il layout per evitare che la richiesta si sovrapponga alla viewport, al logo Google o ai pulsanti allineati in basso.

Il delegato controlla anche il modo in cui i prompt entrano ed escono dallo schermo. Per maggiori dettagli, consulta Personalizzazione dei prompt.

Gestione delle dimensioni dello schermo e della modalità widescreen

Per gestire le modifiche alle dimensioni della mappa e all'orientamento dello schermo, il delegato del layout utilizza le seguenti funzionalità:

  • Ridimensionamento della visualizzazione: regola il layout ogni volta che le dimensioni fisiche della mappa cambiano.

  • Modalità estesa: passa a una variante di layout in formato esteso quando la mappa è abbastanza larga.

Risposta al ridimensionamento della visualizzazione

I layout con schermo diviso, i cursori dei container e la rotazione del dispositivo attivano comunemente il callback sizeChanged(navMap:newState:). Utilizza questo callback per apportare modifiche responsive generali alla tua UI personalizzata. Implementa sizeChanged(navMap:newState:) per riposizionare gli elementi personalizzati per un nuovo screen aspect ratio, applica i tuoi breakpoint di larghezza o altezza personalizzati e rileva quando un evento di ridimensionamento attiva la modalità ampia per scambiare in sicurezza le varianti di layout.

Informazioni sulla modalità larga

La modalità estesa si attiva quando il contenitore della mappa è abbastanza largo da visualizzare i componenti dell'interfaccia utente affiancati.

Leggi il valore booleano wideMode dall'oggetto di stato per riposizionare gli elementi UI personalizzati e mantenere il centro della mappa libero per il conducente. Poiché i componenti forniti da Google, come la scheda di svolta e la scheda ETA, si riducono e si rimodellano automaticamente in modalità widescreen, la lettura di questo valore booleano garantisce che il layout si adatti esattamente nello stesso momento in cui vengono aggiornati i componenti Google.

Considera i seguenti esempi per il posizionamento dei componenti del layout nelle modalità standard e ampia:

  • Modalità Ritratto standard: posiziona la scheda di svolta nella parte superiore dello schermo e la scheda ETA nella parte inferiore.

  • Modalità Wide: mantieni la scheda di svolta e la scheda ETA lungo il bordo principale dello schermo, con la scheda di svolta in alto e la scheda ETA in basso. Se una delle due schede viene posizionata lontano dal bordo anteriore, il comportamento non è definito. Puoi posizionare l'area visibile e i tuoi contenuti ovunque; una scelta comune è il lato finale delle schede.

Controllo della modalità larga durante le transizioni di stato

Se il tuo layout supporta una variante in modalità estesa, valuta il valore booleano wideMode all'interno di ogni callback di transizione di stato, anziché fare affidamento solo su sizeChanged(navMap:newState:).

Controlla l'oggetto state durante i callback come enableNavigationUI(navMap:newState:) e startGuidance(navMap:newState:oldState:) per gestire i lanci iniziali dell'app in orientamento orizzontale. Questo approccio garantisce che il layout valuti lo stato della modalità estesa e attivi la variante di layout standard o estesa corretta nel momento in cui inizia un nuovo stato di navigazione.

Allineamento allo stile di Google

Per fare in modo che le tue visualizzazioni personalizzate corrispondano alla spaziatura dei componenti forniti da Google, l'SDK fornisce la classe NavigationStyleValues. Le proprietà della classe restituiscono le misurazioni in punti.

Ad esempio, per posizionare un pulsante personalizzato nell'angolo superiore dello schermo opposto alla scheda di svolta, utilizza NavigationStyleValues.headerTopPadding come margine superiore del pulsante. Il pulsante si allinea quindi al bordo superiore della scheda di svolta.

Sono disponibili le seguenti misurazioni:

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

Le visualizzazioni fornite da Google includono anche margini integrati. Quando impili due visualizzazioni fornite da Google, vincola una all'altra layoutMarginsGuide in modo che i margini non si sommino. Ad esempio, per posizionare il widget della velocità sopra la scheda ETA, vincola l'ancoraggio inferiore di speedWidget a etaCard.layoutMarginsGuide.topAnchor.

Personalizzazione dei prompt

Il delegato del layout controlla il modo in cui i prompt entrano ed escono dallo schermo e quali angoli sono arrotondati.

Prompt di animazione

L'SDK non anima i prompt per te. Il tuo delegato decide come viene visualizzato e scompare ogni prompt:

  • In showPrompt(navMap:newPrompt:), aggiungi il prompt alla visualizzazione mappa in una posizione iniziale, quindi inseriscilo nella posizione corretta. Ad esempio, avvia il prompt appena sotto l'area visibile o dietro un riquadro inferiore con il bordo superiore allineato alla parte superiore del riquadro.
  • In hidePrompt(navMap:oldPrompt:), anima il prompt fuori dalla visualizzazione, quindi rimuovilo dalla superview al termine dell'animazione.
  • In changePrompt(navMap:newPrompt:oldPrompt:), esegui contemporaneamente le animazioni di visualizzazione e di nascondimento per sostituire un prompt con un altro.

L'implementazione di esempio utilizza questo approccio.

Per disattivare i prompt relativi a traffico, percorsi migliori e incidenti, imposta la proprietà shouldDisplayPrompts di GMSNavigator su false.

Arrotondamento degli angoli dei prompt

I prompt sono conformi al protocollo NavigationRoundedCornerView, quindi puoi scegliere quali angoli arrotondare. Per impostazione predefinita, un prompt arrotonda solo gli angoli superiori, il che è adatto per un prompt ancorato al bordo inferiore dello schermo. Se posizioni un prompt sopra altri contenuti, ad esempio un riquadro inferiore, arrotonda tutti e quattro gli angoli:

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

Esempio: implementazione del layout basato su vincoli

L'esempio seguente mostra un delegato di layout di base che utilizza i vincoli di layout automatico per posizionare la scheda di svolta, la scheda ETA, i pulsanti, il logo Google e l'area visibile. Inoltre, fa scorrere i prompt sullo schermo. Per mantenere l'esempio breve, non gestisce la modalità estesa, il widget della velocità o la barra di avanzamento del percorso, quindi è adatto per le app che supportano solo l'orientamento verticale.

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

Per utilizzare il delegato, impostalo nella visualizzazione mappa prima di attivare la navigazione, come descritto nell'elenco di controllo degli elementi essenziali.

Limitazioni e problemi noti

La personalizzazione del layout non si applica a CarPlay, che utilizza modelli di layout fissi. Se chiami setLayoutDelegate(_:) su una visualizzazione mappa creata per uno schermo CarPlay, il metodo genera un errore.

Per i problemi che interessano versioni specifiche dell'SDK, consulta Problemi noti nelle note di rilascio.