Personalizar o layout de navegação

Exemplo de animação mostrando um layout de navegação personalizado no iOS

Observação: este documento usa convenções de nomenclatura do Swift para classes, protocolos e APIs.

Use a personalização de layout para controlar a hierarquia de visualização e o posicionamento na tela das visualizações fornecidas pelo Google e das suas próprias visualizações de negócios personalizadas.

Em vez de adicionar visualizações personalizadas em slots de layout predefinidos, implemente um delegado de layout para gerenciar o layout de toda a tela. Durante as transições de estado de navegação, o delegado de layout recebe os componentes fornecidos pelo Google, como o cartão de conversão, o cartão de HEC e os botões. Posicione esses elementos ao lado do seu conteúdo personalizado usando as restrições padrão do Auto Layout do iOS.

Use esse framework para posicionar informações comerciais personalizadas, como status do pedido ou instruções de retirada, com precisão na tela, evitando que as visualizações se sobreponham.

Como funciona a personalização de layout

A personalização de layout usa um padrão de design de delegação. Em vez de desenhar ou posicionar automaticamente os componentes da interface na tela, o SDK os transmite diretamente para uma classe personalizada que você escreve: o delegado de layout.

Para personalizar o layout, crie uma classe que esteja em conformidade com o protocolo NavigationLayoutDelegate e transmita uma instância para o método setLayoutDelegate(_:) do seu GMSMapView. Sempre que o estado de navegação muda, como ao passar do mapa de base para a navegação guiada ativa, o SDK executa os métodos de callback no seu delegado e fornece os componentes de interface disponíveis para esse estado específico.

Para oferecer uma experiência do usuário integrada e otimizada, esse framework estabelece uma separação clara de responsabilidades entre o app e o SDK. Use o delegado de layout para personalizar o seguinte:

  • Crie a hierarquia de visualização: escolha exatamente quais componentes do Google e visualizações de negócios personalizadas serão adicionados à tela para cada estado da interface.
  • Posicione cada elemento: defina âncoras de tela, margens e posicionamento de layout precisos. Evite aplicar restrições de largura ou altura personalizadas aos componentes do Google, porque eles calculam as próprias dimensões internas.
  • Enquadrar o mapa de base: defina os limites visíveis da câmera do mapa usando as coordenadas do componente da janela de visualização.
  • Camadas na tela: decida se as visualizações personalizadas vão flutuar acima, abaixo ou ao lado dos controles integrados do Google.

Enquanto isso, não é possível personalizar os seguintes componentes do Google com o delegado de layout:

  • Dimensões do componente: o tamanho e as dimensões internas dos componentes fornecidos pelo Google, que o SDK calcula automaticamente.
  • Condições de acionamento: quando alertas ou avisos dinâmicos aparecem com base em dados de trajeto em tempo real.

Princípios de implementação

Ao escrever um delegado de layout, lembre-se das seguintes regras para evitar bugs de layout ou falhas de ambiente de execução:

  • O padrão é uma tela vazia: os componentes fornecidos pelo Google só aparecem se o delegado os adicionar explicitamente à hierarquia de visualização e os posicionar.
  • As APIs legadas não são compatíveis: as propriedades e os métodos que controlam o modelo de layout legado baseado em slots não serão compatíveis e podem não se comportar como esperado quando um delegado personalizado estiver ativo.
  • Não modifique as estruturas de visualização interna: Não percorra nem modifique as hierarquias de visualização dos componentes fornecidos pelo Google (como o card de conversão ou o cartão HEC). Como essas hierarquias de visualização interna são detalhes de implementação subjacentes, elas estão sujeitas a mudanças nas versões do SDK. Modificá-los pode causar problemas no layout durante as próximas atualizações do SDK.

Compatibilidade com APIs legadas

Para um comportamento de layout confiável ao usar um delegado de layout personalizado, evite usar as seguintes APIs legadas baseadas em slot, que estão planejadas para descontinuação. Para migrar um app, substitua o uso dessas APIs por código no delegado de layout personalizado:

Mostrar APIs legadas baseadas em slots

API Legacy Substituição do delegado de layout
isNavigationHeaderEnabled Leia a visualização turnCard em startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled Leia a visualização etaCard em startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled Leia a visualização tripProgressBar em startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled Localize o botão .reporting na matriz buttons.
setHeaderAccessoryView(_:) e invalidateLayout(forAccessoryView:) Adicione sua própria visualização ao layout e fixe-a na turnCard.
padding e paddingAdjustmentBehavior Posicione a visualização viewport para enquadrar a câmera.
navigationHeaderLayoutGuide Ancore suas visualizações personalizadas diretamente no turnCard.
navigationFooterLayoutGuide Ancore suas visualizações personalizadas diretamente no etaCard.
bottomTrailingButtonsLayoutGuide Ancore os botões diretamente nas guias do etaCard ou do contêiner.
bottomTrailingButtonsAlternateLayoutGuide Ancore os botões diretamente no botão .reporting ou nas restrições de wideMode.
navigatorWillPresentPrompt(_:) e navigatorDidDismissPrompt(_:) Use os callbacks de comando NavigationLayoutDelegate, como showPrompt(navMap:newPrompt:).

Enquanto um delegado de layout personalizado estiver definido, os guias de layout legados (como navigationHeaderLayoutGuide ou bottomTrailingButtonsLayoutGuide) e os métodos de visualização de acessórios de cabeçalho não terão efeito. Em vez disso, ancore suas visualizações às fornecidas pelo Google.

A propriedade compassButton de GMSUISettings ainda controla a bússola enquanto a interface de navegação está desativada. Enquanto a navegação estiver ativada, o delegado vai decidir se a bússola será mostrada na matriz buttons.

Lista de verificação do Essentials

Siga estas etapas e requisitos essenciais para implementar o delegado de layout com sucesso:

  • Defina o delegado antes de ativar a navegação: chame setLayoutDelegate(_:) no GMSMapView antes de ativar a navegação com isNavigationEnabled ou enableNavigation(with:). O método vai gerar um erro se a navegação já estiver ativada ou se você tiver criado a visualização de mapa para uma tela do CarPlay. A visualização de mapa mantém uma referência forte ao seu delegado. Para restaurar o layout integrado, transmita nil enquanto a navegação estiver desativada.

    // 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
        
  • Anexar visualizações obrigatórias: adicione o logotipo do Google (googleLogo) e a janela de visualização (viewport) ao contêiner de layout em todos os estados de navegação.

  • Respeite as dimensões fixas: o SDK determina as dimensões de vários componentes fornecidos pelo Google. Não aplique restrições de largura ou altura personalizadas aos seguintes elementos:

    • Card de direção
    • Cartão HEC
    • Comandos recebidos
    • Logotipo do Google
    • Widget de velocidade
  • Adicionar elementos como subvisualizações: adicione as visualizações fornecidas pelo Google diretamente como subvisualizações à sua visualização de contêiner personalizada ou ao próprio GMSMapView.

  • Alinhar usando guias de layout: as visualizações fornecidas pelo Google incluem margens visuais integradas. Para alinhar perfeitamente seus elementos personalizados com os componentes fornecidos pelo Google, restrinja suas visualizações à propriedade layoutMarginsGuide da visualização do Google.

  • Limpar ao sair do estado: em stopGuidance(navMap:newState:oldState:), remova as visualizações de orientação ativas e desative as restrições delas antes de restaurar o layout pronto para navegação. Em disableNavigationUI(navMap:oldState:), remova todas as visualizações e guias de layout que você adicionou, incluindo qualquer solicitação na tela. Isso evita que as visualizações de um estado se sobreponham ao próximo.

  • Suporte próprio para acessibilidade e leitura da direita para a esquerda: as visualizações fornecidas pelo Google atendem aos padrões de acessibilidade, mas o delegado é responsável pela acessibilidade do layout geral. Para oferecer suporte a idiomas da direita para a esquerda, use âncoras iniciais e finais em vez de âncoras à esquerda e à direita.

  • Evite APIs de layout legadas: não use propriedades legadas que modificam o layout em GMSUISettings ou guias de layout legados em GMSMapView enquanto um delegado personalizado estiver ativo. O SDK pode ignorar essas chamadas legadas quando um delegado personalizado é anexado.

Estados da interface e componentes do Google

Quando ocorre uma transição de estado de navegação, o SDK transmite um objeto NavigationUIState somente leitura para o callback do delegado. Esse objeto agrupa as flags de configuração de layout atuais com os componentes do Google (por exemplo, instâncias de UIView) necessários para renderizar a tela.

O delegado de layout gerencia a hierarquia de visualização em quatro estados operacionais. O diagrama de estado a seguir ilustra como o SDK faz a transição entre os estados de navegação e quais métodos de callback ele executa no seu delegado:

Diagrama de transição de estado do ciclo de vida do NavigationLayoutDelegate mostrando as transições entre os estados Sem navegação, Pronto para navegação e Orientação ativa.

Os componentes específicos do Google fornecidos ao seu delegado dependem da fase atual do ciclo de vida da navegação.

Componentes obrigatórios (todos os estados de navegação)

Independentemente da fase de navegação atual, é necessário incluir, posicionar e manter os seguintes componentes do Google visíveis na sua hierarquia de visualização:

  • Logotipo do Google (googleLogo): esse componente mostra o logotipo obrigatório do Google Maps. Se o botão Recentralizar estiver ativado, o logotipo vai se transformar automaticamente nesse botão quando o motorista rolar o mapa para longe do veículo. Como resultado, o posicionamento do logotipo determina onde o botão Recentralizar aparece. Recomendamos colocar o logotipo no canto inferior esquerdo do layout para corresponder às expectativas padrão da interface do mapa.

  • Janela de visualização (viewport): um UIView invisível que define os limites de enquadramento da câmera. O posicionamento da janela de visualização determina exatamente onde o SDK centraliza o chevron do veículo e desenha a linha de trajeto ativa. Posicione a janela de visualização para que ela cubra a área aberta e desobstruída da tela, evitando sobreposições opacas, como páginas inferiores personalizadas.

Durante o estado de navegação, a interface permanece mínima para manter o foco no mapa de base. O objeto NavigationReadyUIState fornece acesso ao seguinte:

  • buttons: uma matriz de visualizações de ação flutuante configuradas para o mapa de base (geralmente apenas o botão "Bússola"). Como o Google pode adicionar ou reordenar botões em versões futuras do SDK, evite usar índices de matriz fixos. Em vez disso, faça uma iteração na matriz, leia o tipo de cada botão comparando a propriedade type com NavigationButtonKnownType.compass e extraia a visualização física chamando a propriedade view. Os botões exatos nessa matriz são diferentes daqueles disponíveis durante a navegação ativa.

Componentes de orientação ativa

Quando a navegação guiada começa, o SDK desbloqueia o conjunto completo de controles de navegação. O objeto ActiveGuidanceUIState fornece acesso aos seguintes componentes do Google:

  • turnCard: o banner do cabeçalho principal que mostra as próximas instruções de manobra, as medições de distância e as orientações de faixa. Coloque-a na parte de cima do layout para estabelecer uma hierarquia de navegação familiar e ancorar as visualizações personalizadas ao redor dela.

  • etaCard: o banner de rodapé que mostra o horário estimado de chegada, o tempo restante da viagem e a distância restante até o destino. Coloque-o na parte de baixo da tela ou integre as coordenadas dele às suas planilhas personalizadas de gerenciamento de tarefas.

  • tripProgressBar: uma barra de progresso vertical que mostra ao motorista a distância percorrida no trajeto atual. Ao contrário dos layouts legados, que fixavam isso estritamente na borda do mapa, você tem total liberdade para fixá-lo em qualquer lugar, por exemplo, ao longo de uma borda lateral do seu contêiner personalizado.

  • speedWidget: um controle flutuante para o velocímetro e os limites de velocidade informados. Dependendo das configurações da API (as propriedades shouldDisplaySpeedometer e shouldDisplaySpeedLimit de GMSMapView) e da disponibilidade de dados, essa visualização é redimensionada dinamicamente entre quatro estados visuais no momento da execução (não mostrando nada, apenas a velocidade atual, apenas o limite de velocidade ou ambas as leituras). Como o widget pode mudar de tamanho sem aviso prévio, sempre use restrições para fixar as visualizações ao redor, de modo que o layout se adapte automaticamente para evitar sobreposições espaciais.

  • buttons: uma matriz expandida de visualizações de ação flutuante para o estado de orientação ativo (normalmente incluindo a bússola e o botão de alerta de incidente). Assim como no estado pronto para navegação, é possível localizar e extrair visualizações de botões individuais filtrando a matriz por type (.compass ou .reporting) e extraindo a visualização física usando a propriedade view. Em seguida, você pode posicioná-los de forma independente ou adicioná-los a um GMSNavigationHidingStackView. Essa visualização de pilha organiza os botões verticalmente e, quando o espaço acaba, os oculta a partir do final da matriz arrangedSubviews. Não defina isHidden nas visualizações que ele gerencia.
Dica: prática recomendada para enquadramento da câmera. A visualização do card de curva tem uma altura fixa com base em NavigationStyleValues.headerNominalHeight, que é alta o suficiente para qualquer conteúdo do card de curva. Para manter o enquadramento da câmera estável, restrinja a parte de cima de viewport diretamente à borda de baixo de turnCard, conforme mostrado no exemplo de implementação.

Componentes de comandos dinâmicos

Os avisos, como alertas de incidentes ou de radares, são acionados de forma independente durante a navegação ativa.

Quando uma solicitação está pronta para ser mostrada, o SDK chama o callback showPrompt(navMap:newPrompt:) do delegado e transmite uma visualização newPrompt diretamente. O delegado é responsável por posicionar esse aviso sobre o layout (normalmente fixado na borda inferior do contêiner do mapa).

Como o aviso de entrada cobre a parte de baixo da tela, você precisa atualizar o layout para evitar que ele se sobreponha à janela de visualização, ao logotipo do Google ou a qualquer botão alinhado na parte de baixo.

O delegado também controla como os comandos aparecem e desaparecem da tela. Para mais detalhes, consulte Como personalizar comandos.

Como lidar com o tamanho da tela e o modo amplo

Para lidar com mudanças nas dimensões do mapa e na orientação da tela, o delegado de layout usa os seguintes recursos:

  • Redimensionamento da visualização: ajusta o layout sempre que o tamanho físico do mapa muda.

  • Modo amplo: muda para uma variante de layout de formato amplo quando o mapa é grande o suficiente.

Como responder ao redimensionamento da visualização

Layouts de tela dividida, controles deslizantes de contêiner e rotação do dispositivo geralmente acionam o callback sizeChanged(navMap:newState:). Use esse callback para fazer ajustes responsivos gerais na interface personalizada. Implemente sizeChanged(navMap:newState:) para reposicionar elementos personalizados para uma nova proporção de tela, aplique seus próprios pontos de interrupção de largura ou altura personalizados e detecte quando um evento de redimensionamento ativa o modo amplo para trocar variantes de layout com segurança.

Noções básicas sobre o modo amplo

O modo amplo é ativado quando o contêiner do mapa é largo o suficiente para mostrar os componentes da interface lado a lado.

Leia o booleano wideMode do objeto de estado para reposicionar os elementos da interface personalizados e manter o centro do mapa livre para o motorista. Como os componentes fornecidos pelo Google, como o cartão de curva e o cartão HEC, são reduzidos e remodelados automaticamente no modo amplo, a leitura desse booleano garante que o layout se adapte no mesmo momento em que os componentes do Google são atualizados.

Considere os exemplos a seguir para posicionar os componentes de layout nos modos padrão e amplo:

  • Modo retrato padrão: posicione o card de curva na parte de cima da tela e o cartão HEC na parte de baixo.

  • Modo amplo: mantenha o cartão de conversão e o cartão HEC ao longo da borda principal da tela, com o cartão de conversão na parte de cima e o cartão HEC na parte de baixo. Colocar qualquer um dos cards longe da borda principal resulta em um comportamento indefinido. Você pode colocar a janela de visualização e seu próprio conteúdo em qualquer lugar. Uma escolha comum é o lado final dos cards.

Verificação do modo amplo durante as transições de estado

Se o layout for compatível com uma variação de modo amplo, avalie o booleano wideMode em cada callback de transição de estado, em vez de depender apenas de sizeChanged(navMap:newState:).

Verifique o objeto de estado durante callbacks como enableNavigationUI(navMap:newState:) e startGuidance(navMap:newState:oldState:) para processar as inicializações do app no modo paisagem. Essa abordagem garante que o layout avalie o estado do modo amplo e ative a variante de layout padrão ou ampla correta no momento em que um novo estado de navegação começar.

Alinhamento com o estilo do Google

Para ajudar suas visualizações personalizadas a corresponder ao espaçamento dos componentes fornecidos pelo Google, o SDK oferece a classe NavigationStyleValues. As propriedades de classe retornam medições em pontos.

Por exemplo, para colocar um botão personalizado no canto superior da tela oposto ao card de curva, use NavigationStyleValues.headerTopPadding como a margem superior do botão. O botão se alinha com a borda superior do card de curva.

As seguintes medições estão disponíveis:

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

As visualizações fornecidas pelo Google também incluem margens integradas. Ao empilhar duas visualizações fornecidas pelo Google, restrinja uma à layoutMarginsGuide da outra para que as margens não se somem. Por exemplo, para colocar o widget de velocidade acima do cartão de HEC, restrinja a âncora inferior de speedWidget a etaCard.layoutMarginsGuide.topAnchor.

Personalizar comandos

O delegado de layout controla como as solicitações aparecem e desaparecem da tela e quais cantos são arredondados.

Comandos de animação

O SDK não anima as solicitações para você. O delegado decide como cada solicitação aparece e desaparece:

  • Em showPrompt(navMap:newPrompt:), adicione a solicitação à visualização do mapa em uma posição inicial e anime-a para que ela se mova até o lugar certo. Por exemplo, inicie o prompt logo abaixo da área visível ou atrás de uma página inferior com a borda superior alinhada à parte de cima da página.
  • Em hidePrompt(navMap:oldPrompt:), anime a saída da solicitação e, em seguida, remova-a da supervisualização quando a animação terminar.
  • Em changePrompt(navMap:newPrompt:oldPrompt:), execute as animações de ocultar e mostrar ao mesmo tempo para substituir uma solicitação por outra.

O exemplo de implementação usa essa abordagem.

Para desativar as solicitações de trânsito, trajetos melhores e incidentes, defina a propriedade shouldDisplayPrompts de GMSNavigator como false.

Arredondamento dos cantos do comando

As solicitações estão em conformidade com o protocolo NavigationRoundedCornerView, então é possível escolher quais cantos serão arredondados. Por padrão, um aviso arredonda apenas os cantos superiores, o que é adequado para um aviso fixado na borda inferior da tela. Se você colocar uma solicitação acima de outro conteúdo, como uma página inferior, arredonde todos os quatro cantos:

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

Exemplo: implementação de layout baseado em restrições

O exemplo a seguir mostra um delegado de layout básico que usa restrições de layout automático para posicionar o cartão de conversão, o cartão de HEC, os botões, o logotipo do Google e a janela de visualização. Ele também desliza os comandos para dentro e para fora da tela. Para manter o exemplo curto, ele não lida com o modo amplo, o widget de velocidade ou a barra de progresso da viagem. Portanto, ele é adequado para apps que aceitam apenas a orientação retrato.

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

Para usar o delegado, defina-o na visualização do mapa antes de ativar a navegação, conforme descrito na lista de verificação de itens essenciais.

Limitações e problemas conhecidos

A personalização de layout não se aplica ao CarPlay, que usa modelos de layout fixos. Se você chamar setLayoutDelegate(_:) em uma visualização de mapa criada para uma tela do CarPlay, o método vai gerar um erro.

Para problemas que afetam versões específicas do SDK, consulte Problemas conhecidos nas notas da versão.