Как настроить макет навигации

Анимация, показывающая пример настроенного макета навигации на iOS

Примечание. В этом документе используются соглашения Swift о наименовании классов, протоколов и API.

Используйте настройку макета для управления иерархией View и размещением на экране как для представлений Google, так и для собственных представлений.

Вместо того чтобы добавлять специальные представления в предопределенные слоты макета, реализуйте делегат макета, который будет управлять макетом всего экрана. При переходах между состояниями навигации делегат макета получает компоненты, предоставляемые Google, такие как карточка поворота, карточка расчетного времени прибытия и кнопки. Разместите эти элементы рядом с собственным контентом, используя стандартные ограничения автоматического макета iOS.

Используйте этот фреймворк, чтобы размещать на экране информацию о компании, например состояние заказа или инструкции по самовывозу, и избегать наложения представлений.

Как работает настройка макета

Для настройки макета используется шаблон проектирования делегирования. Вместо того чтобы автоматически рисовать или размещать компоненты интерфейса на экране, SDK передает их непосредственно в созданный вами пользовательский класс – делегат макета.

Чтобы настроить макет, создайте класс, соответствующий протоколу NavigationLayoutDelegate, и передайте экземпляр в метод setLayoutDelegate(_:) объекта GMSMapView. При каждом переходе между состояниями навигации, например при переходе от базовой карты к активному пошаговому ведению по маршруту, SDK выполняет методы обратного вызова для вашего делегата и предоставляет компоненты интерфейса, доступные для этого конкретного состояния.

Чтобы обеспечить удобство для пользователей, этот фреймворк четко разделяет ответственность между вашим приложением и SDK. Используйте делегат макета, чтобы настроить следующее:

  • Создайте иерархию View. Выберите, какие компоненты Google и пользовательские представления нужно добавить на экран для каждого состояния интерфейса.
  • Размещайте элементы на экране. Задавайте точные привязки, поля и расположение элементов. Не задавайте для компонентов Google собственные ограничения по ширине и высоте, поскольку они рассчитывают свои размеры самостоятельно.
  • Кадрирование базовой карты. Определите видимые границы камеры карты, используя координаты компонента области просмотра.
  • Слои экрана. Решите, где будут находиться пользовательские представления: над, под или рядом со встроенными элементами управления Google.

При этом вы не можете настраивать следующие компоненты Google с помощью делегата макета:

  • Размеры компонентов. Размер и внутренние размеры компонентов, предоставляемых Google, которые SDK рассчитывает автоматически.
  • Условия активации. Когда на основе данных о маршруте в реальном времени появляются динамические оповещения или подсказки.

Принципы реализации

При написании делегата макета помните о следующих правилах, чтобы предотвратить ошибки макета или сбои во время выполнения:

  • По умолчанию экран пуст. Компоненты Google появляются, только если делегат явно добавляет их в иерархию View и размещает их.
  • Устаревшие API не поддерживаются. Свойства и методы, управляющие устаревшей моделью макета на основе рекламных мест, не будут поддерживаться и могут работать некорректно, если активен собственный делегат.
  • Не изменяйте внутренние структуры представлений. Не изменяйте иерархии представлений компонентов, предоставляемых Google, например карточки поворота или карточки расчетного времени прибытия. Поскольку эти внутренние иерархии View являются деталями реализации, они могут меняться в разных выпусках SDK. Если вы измените их, то при последующих обновлениях SDK макет может перестать работать.

Совместимость с устаревшим API

Чтобы макеты работали надежно, не используйте следующие устаревшие API на основе рекламных мест, которые планируется упразднить: Чтобы перенести существующее приложение, замените использование этих API кодом в делегате пользовательского макета:

Показать устаревшие API на основе рекламных мест

Устаревший API Замена делегата макета
isNavigationHeaderEnabled Прочитайте представление turnCard в startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled Прочитайте представление etaCard в startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled Прочитайте представление tripProgressBar в startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled Найдите кнопку .reporting в массиве buttons.
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.

Свойство compassButton объекта GMSUISettings по-прежнему управляет компасом, даже если интерфейс навигации отключен. Когда навигация включена, делегат решает, показывать ли компас из массива buttons.

Контрольный список Essentials

Чтобы успешно реализовать делегат макета, выполните следующие действия и требования:

  • Настройте делегат, прежде чем включать навигацию. Вызовите setLayoutDelegate(_:) для GMSMapView, прежде чем включать навигацию с помощью 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, привяжите их к свойству layoutMarginsGuide представления Google.

  • Очистка при выходе из состояния. В методе stopGuidance(navMap:newState:oldState:) удалите активные представления подсказок и деактивируйте их ограничения, прежде чем восстанавливать макет, готовый к навигации. В disableNavigationUI(navMap:oldState:) удалите все добавленные представления и направляющие макета, включая подсказки на экране. Это предотвращает наложение представлений из одного состояния на другое.

  • Специальные возможности и поддержка письма справа налево. Предоставляемые Google представления соответствуют стандартам специальных возможностей, но за специальные возможности макета в целом отвечает делегат. Чтобы обеспечить поддержку языков с написанием справа налево, используйте начальные и конечные привязки вместо левых и правых.

  • Не используйте устаревшие API макета. Не используйте устаревшие свойства, изменяющие макет, в GMSUISettings или устаревшие руководства по макету в GMSMapView, пока активен пользовательский делегат. Если прикреплен пользовательский делегат, SDK может игнорировать эти устаревшие вызовы.

Состояния интерфейса и компоненты Google

При изменении состояния навигации SDK передает объект NavigationUIState, доступный только для чтения, обратному вызову делегата. Этот объект объединяет текущие флаги конфигурации макета с компонентами Google (например, экземплярами UIView), которые необходимы для отрисовки экрана.

Делегат макета управляет иерархией View в четырех рабочих состояниях. На следующей диаграмме показано, как SDK переходит между состояниями навигации и какие методы обратного вызова он выполняет для вашего делегата:

Диаграмма переходов между состояниями жизненного цикла NavigationLayoutDelegate, показывающая переходы между состояниями Non-Navigation, Navigation Ready и Active Guidance.

Конкретные компоненты Google, предоставляемые вашему делегату, зависят от текущей фазы жизненного цикла навигации.

Обязательные компоненты (все состояния навигации)

Независимо от текущего этапа навигации, вы должны включить, разместить и оставить видимыми в иерархии View следующие компоненты Google:

  • Логотип Google (googleLogo). Этот компонент показывает обязательный логотип Google Карт. Если кнопка Вернуть в центр включена, логотип автоматически превращается в эту кнопку, когда водитель прокручивает карту в сторону от своего автомобиля. Таким образом, от расположения логотипа зависит, где будет находиться кнопка Центрировать. Рекомендуется размещать логотип в нижнем левом углу макета, чтобы он соответствовал стандартному интерфейсу карты.

  • Область просмотра (viewport). Невидимый объект UIView, который определяет границы кадрирования камеры. От того, как вы разместите область просмотра, зависит, где SDK разместит шеврон транспортного средства и нарисует линию активного маршрута. Разместите область просмотра так, чтобы она покрывала открытую, ничем не заслоненную часть экрана, не заходя на непрозрачные наложения, например настраиваемые нижние экраны.

В режиме готовности к навигации интерфейс остается минималистичным, чтобы не отвлекать внимание от базовой карты. Объект NavigationReadyUIState предоставляет доступ к следующим элементам:

  • buttons: массив плавающих кнопок действий, настроенных для базовой карты (обычно только кнопка компаса). В будущих версиях SDK Google может добавить или переставить кнопки, поэтому не полагайтесь на фиксированные индексы массивов. Вместо этого выполните итерацию по массиву, определите тип каждой кнопки, сравнив ее свойство type со значением NavigationButtonKnownType.compass, и извлеките физическое представление, вызвав свойство view. Обратите внимание, что кнопки в этом массиве отличаются от тех, которые доступны во время активного ведения по маршруту.

активных компонента руководства

Когда начинается пошаговая навигация, SDK разблокирует полный набор элементов управления навигацией. Объект ActiveGuidanceUIState предоставляет доступ к следующим компонентам Google:

  • turnCard – основной баннер в заголовке, на котором показываются указания по предстоящим маневрам, расстояния и подсказки по выбору полосы. Разместите его в верхней части макета, чтобы создать привычную иерархию навигации и закрепить вокруг него пользовательские представления.

  • etaCard. Нижний баннер, на котором показано расчетное время прибытия, оставшееся время в пути и расстояние до пункта назначения. Разместите его в нижней части экрана или интегрируйте его координаты с вашими таблицами управления задачами.

  • tripProgressBar. Вертикальный индикатор выполнения, который показывает водителю, какую часть маршрута он уже проехал. В отличие от устаревших макетов, в которых он был жестко привязан к краю карты, теперь вы можете закрепить его в любом месте, например вдоль бокового края пользовательского контейнера.

  • speedWidget. Плавающий элемент управления для спидометра и ограничений скорости. В зависимости от настроек API (свойств shouldDisplaySpeedometer и shouldDisplaySpeedLimit объекта GMSMapView) и доступности данных этот вид динамически изменяет размер между четырьмя визуальными состояниями во время выполнения (ничего не отображается, только текущая скорость, только ограничение скорости или оба показания). Поскольку виджет может переключаться между этими размерами без уведомления, всегда привязывайте окружающие представления с помощью ограничений, чтобы ваш макет автоматически адаптировался и предотвращал пространственные перекрытия.

  • buttons. Расширенный массив плавающих кнопок для активного состояния ведения по маршруту (обычно включает компас и кнопку сообщения о дорожном инциденте). Как и в состоянии готовности к навигации, вы можете найти и извлечь отдельные представления кнопок, отфильтровав массив по type (.compass или .reporting) и извлекая физическое представление с помощью его свойства view. Затем вы можете разместить их по отдельности или добавить в GMSNavigationHidingStackView. Этот стек располагает кнопки вертикально и, когда место заканчивается, скрывает их, начиная с конца массива arrangedSubviews. Не задавайте isHidden для представлений, которыми он управляет.
Совет. Как правильно настроить камеру. Высота карточки поворота фиксирована и определяется значением NavigationStyleValues.headerNominalHeight. Этого достаточно для любого контента. Чтобы кадрирование камеры было стабильным, привяжите верхнюю часть viewport непосредственно к нижнему краю turnCard, как показано в примере реализации.

компонента динамического запроса

Подсказки, например оповещения об инцидентах или предупреждения о камерах контроля скорости, срабатывают независимо во время активного ведения по маршруту.

Когда запрос готов к показу, SDK вызывает обратный вызов showPrompt(navMap:newPrompt:) вашего делегата и передает представление newPrompt напрямую. Ваш делегат отвечает за плавное размещение этого запроса над вашим макетом (обычно он привязан к нижнему краю контейнера карты).

Поскольку входящее приглашение закрывает нижнюю часть экрана, вам необходимо обновить макет, чтобы приглашение не перекрывало область просмотра, логотип Google или любые кнопки, выровненные по нижнему краю.

Делегат также управляет тем, как подсказки появляются на экране и исчезают с него. Подробнее о настройке запросов…

Как обрабатывать размер экрана и широкий режим

Чтобы обрабатывать изменения размеров карты и ориентации экрана, делегат макета использует следующие функции:

  • Изменение размера области просмотра. Макет корректируется при изменении физического размера карты.

  • Широкий режим. Переключается на широкоформатный вариант макета, когда карта достаточно широкая.

Как реагировать на изменение размера представления

Макеты с разделенным экраном, ползунки контейнеров и поворот устройства обычно вызывают обратный вызов sizeChanged(navMap:newState:). Используйте этот обратный вызов, чтобы внести общие корректировки в пользовательский интерфейс. Реализуйте sizeChanged(navMap:newState:), чтобы перемещать пользовательские элементы для нового соотношения сторон экрана, применять собственные контрольные точки ширины или высоты и определять, когда событие изменения размера переключает широкий режим, чтобы безопасно менять варианты макета.

О широкоэкранном режиме

Широкий режим активируется, когда контейнер карты достаточно широк, чтобы отображать компоненты пользовательского интерфейса рядом друг с другом.

Прочитайте логическое значение wideMode из объекта состояния, чтобы изменить положение пользовательских элементов интерфейса и освободить центр карты для водителя. Поскольку компоненты Google, такие как карточка поворота и карточка расчетного времени прибытия, автоматически уменьшаются и меняют форму в широком режиме, чтение этого логического значения гарантирует, что ваш макет адаптируется в тот же момент, когда обновляются компоненты Google.

Ниже приведены примеры размещения компонентов макета в стандартном и широком режимах.

  • Стандартный вертикальный режим. Карточка поворота находится в верхней части экрана, а карточка времени прибытия – в нижней.

  • Широкий режим. Карточки поворота и времени прибытия должны находиться у ведущего края экрана. Карточка поворота должна быть сверху, а карточка времени прибытия – снизу. Если разместить любую из карт вдали от переднего края, это приведет к неопределенному поведению. Вы можете разместить область просмотра и собственный контент в любом месте. Обычно выбирают нижнюю часть карточек.

Проверка широкого режима при переходах между состояниями

Если макет поддерживает вариант широкого режима, оценивайте логическое значение wideMode внутри каждого обратного вызова перехода состояния, а не полагайтесь только на sizeChanged(navMap:newState:).

Проверяйте объект состояния во время обратных вызовов, таких как enableNavigationUI(navMap:newState:) и startGuidance(navMap:newState:oldState:), чтобы обрабатывать первоначальные запуски приложения в альбомной ориентации. Этот подход гарантирует, что ваш макет оценивает состояние широкого режима и активирует правильный стандартный или широкий вариант макета в момент начала нового состояния навигации.

Соответствие стилю Google

Чтобы ваши собственные представления соответствовали интервалам компонентов, предоставляемых Google, SDK предоставляет класс NavigationStyleValues. Свойства его класса возвращают измерения в пунктах.

Например, чтобы разместить настраиваемую кнопку в верхнем углу экрана напротив карточки поворота, используйте NavigationStyleValues.headerTopPadding в качестве верхнего поля кнопки. Кнопка выравнивается по верхнему краю карточки поворота.

Доступны следующие показатели:

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

В представлениях, предоставляемых Google, также есть встроенные поля. Если вы размещаете два представления Google друг над другом, привяжите одно из них к layoutMarginsGuide другого, чтобы их поля не складывались. Например, чтобы разместить виджет скорости над карточкой времени прибытия, привяжите нижний якорь speedWidget к etaCard.layoutMarginsGuide.topAnchor.

Как настраивать запросы

Делегат макета управляет тем, как подсказки появляются на экране и исчезают с него, а также тем, какие из их углов закруглены.

Анимированные подсказки

SDK не анимирует подсказки. Ваш представитель решает, как будет появляться и исчезать каждая подсказка:

  • В showPrompt(navMap:newPrompt:) добавьте подсказку в начальную позицию на карте, а затем анимируйте ее перемещение в нужное место. Например, можно разместить подсказку сразу под видимой областью или за нижним экраном, выровняв ее верхний край по верхнему краю экрана.
  • В методе hidePrompt(navMap:oldPrompt:) настройте анимацию, чтобы убрать запрос из области просмотра, а затем удалите его из суперпредставления по окончании анимации.
  • В changePrompt(navMap:newPrompt:oldPrompt:) одновременно запустите анимации скрытия и показа, чтобы заменить одну подсказку другой.

Этот подход используется в примере реализации.

Чтобы отключить подсказки о пробках, лучших маршрутах и инцидентах, задайте для свойства shouldDisplayPrompts объекта GMSNavigator значение false.

Скругление углов подсказки

Подсказки соответствуют протоколу NavigationRoundedCornerView, поэтому вы можете выбрать, какие углы будут закруглены. По умолчанию у подсказки закруглены только верхние углы, что подходит для подсказки, закрепленной у нижнего края экрана. Если вы размещаете подсказку поверх другого контента, например нижнего экрана, закруглите все четыре угла:

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

Пример реализации макета на основе ограничений

В примере ниже показан базовый делегат макета, который использует ограничения Auto Layout для позиционирования карточки поворота, карточки времени прибытия, кнопок, логотипа 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, в котором используются фиксированные шаблоны макетов. Если вы вызовете setLayoutDelegate(_:) в режиме просмотра карты, созданном для экрана CarPlay, метод выдаст ошибку.

Информацию о проблемах, которые затрагивают определенные версии SDK, можно найти в разделе Известные проблемы в примечаниях к выпуску.