Dostosowywanie układu nawigacji

Przykładowa animacja pokazująca dostosowany układ nawigacji na iOS

Uwaga: w tym dokumencie używamy konwencji nazewnictwa Swift dla klas, protokołów i interfejsów API.

Dzięki dostosowywaniu układu możesz kontrolować hierarchię widoków i umieszczanie na ekranie zarówno widoków udostępnianych przez Google, jak i własnych widoków biznesowych.

Zamiast dodawać widoki niestandardowe do wstępnie zdefiniowanych boksów układu, zaimplementuj delegata układu, który będzie zarządzać układem całego ekranu. Podczas zmian stanu nawigacji Twój delegat układu otrzymuje komponenty dostarczone przez Google, takie jak karta skrętu, karta szacowanego czasu dotarcia na miejsce i przyciski. Umieść te elementy obok własnych treści niestandardowych, korzystając ze standardowych ograniczeń automatycznego układu iOS.

Użyj tej platformy, aby precyzyjnie umieścić na ekranie niestandardowe informacje o firmie, takie jak stan zamówienia lub instrukcje odbioru, i zapobiec nakładaniu się widoków.

Jak działa dostosowywanie układu

Dostosowywanie układu korzysta ze wzorca projektowego przekazywania. Zamiast automatycznie rysować lub pozycjonować komponenty interfejsu na ekranie, pakiet SDK przekazuje je bezpośrednio do niestandardowej klasy, którą piszesz: delegata układu.

Aby dostosować układ, utwórz klasę zgodną z protokołem NavigationLayoutDelegate i przekaż instancję do metody setLayoutDelegate(_:) obiektu GMSMapView. Za każdym razem, gdy zmienia się stan nawigacji, np. gdy przechodzisz z mapy podstawowej do aktywnych szczegółowych wskazówek dojazdu, pakiet SDK wykonuje metody wywołania zwrotnego w obiekcie delegowanym i udostępnia komponenty interfejsu dostępne w danym stanie.

Aby zapewnić użytkownikom płynne i zintegrowane działanie, ta platforma wyraźnie rozdziela odpowiedzialność między aplikację a pakiet SDK. Użyj delegata układu, aby dostosować te elementy:

  • Utworzyć hierarchię widoków: wybierz dokładnie, które komponenty Google i niestandardowe widoki biznesowe chcesz dodać do ekranu w przypadku każdego stanu interfejsu.
  • Pozycjonowanie każdego elementu: ustawianie precyzyjnych punktów zakotwiczenia, marginesów i miejsca w układzie. Unikaj stosowania niestandardowych ograniczeń szerokości lub wysokości do komponentów Google, ponieważ komponenty Google obliczają własne wymiary wewnętrzne.
  • Kadrowanie mapy podstawowej: określ widoczne granice kamery mapy za pomocą współrzędnych komponentu widocznego obszaru.
  • Warstwy ekranu: zdecyduj, czy widoki niestandardowe mają być wyświetlane nad, pod czy obok wbudowanych elementów sterujących Google.

Nie możesz jednak dostosować za pomocą delegata układu tych komponentów Google:

  • Wymiary komponentu: rozmiar i wymiary wewnętrzne komponentów dostarczanych przez Google, które pakiet SDK oblicza automatycznie.
  • Warunki aktywacji: kiedy dynamiczne alerty lub prośby pojawiają się na podstawie danych o trasie w czasie rzeczywistym.

 Zasady implementacji

Podczas pisania delegata układu pamiętaj o tych regułach, aby zapobiec błędom układu lub awariom w czasie działania:

  • Domyślnie jest to pusty ekran: komponenty dostarczane przez Google pojawiają się tylko wtedy, gdy delegat wyraźnie doda je do hierarchii widoków i ustawi ich pozycję.
  • Starsze interfejsy API nie są obsługiwane: właściwości i metody, które kontrolują starszy model układu opartego na boksach, nie będą obsługiwane i mogą nie działać zgodnie z oczekiwaniami, gdy aktywny jest niestandardowy obiekt delegowany.
  • Nie modyfikuj wewnętrznych struktur widoku: nie przechodź przez hierarchie widoków komponentów dostarczanych przez Google (takich jak karta skrętu lub karta szacowanego czasu dotarcia na miejsce) ani ich nie modyfikuj. Te wewnętrzne hierarchie widoków są szczegółami implementacji, więc mogą się zmieniać w poszczególnych wersjach pakietu SDK. Ich zmodyfikowanie może spowodować uszkodzenie układu podczas przyszłych aktualizacji pakietu SDK.

Zgodność ze starszą wersją interfejsu API

Aby zapewnić niezawodne działanie układu podczas korzystania z niestandardowego delegata układu, unikaj używania tych starszych interfejsów API opartych na boksach, które mają zostać wycofane. Aby przenieść istniejącą aplikację, zastąp użycie tych interfejsów API kodem w niestandardowym delegacie układu:

Pokaż starsze interfejsy API oparte na boksach

Starszy interfejs API Zastąpienie delegata układu
isNavigationHeaderEnabled Odczytaj widok turnCard w startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled Odczytaj widok etaCard w startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled Odczytaj widok tripProgressBar w startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled Znajdź przycisk .reporting w tablicy buttons.
setHeaderAccessoryView(_:) i invalidateLayout(forAccessoryView:) Dodaj do układu własny widok i zakotwicz go w turnCard.
padding i paddingAdjustmentBehavior Ustaw widok viewport, aby wykadrować kamerę.
navigationHeaderLayoutGuide Przypnij widoki niestandardowe bezpośrednio do turnCard.
navigationFooterLayoutGuide Przypnij widoki niestandardowe bezpośrednio do etaCard.
bottomTrailingButtonsLayoutGuide Zakotwicz przyciski bezpośrednio w etaCard lub prowadnicach kontenera.
bottomTrailingButtonsAlternateLayoutGuide Zakotwicz przyciski bezpośrednio w ograniczeniach przycisku .reporting lub wideMode.
navigatorWillPresentPrompt(_:) i navigatorDidDismissPrompt(_:) Polegaj na wywołaniach zwrotnych promptów NavigationLayoutDelegate, takich jak showPrompt(navMap:newPrompt:).

Gdy ustawiony jest niestandardowy delegat układu, starsze prowadnice układu (takie jak navigationHeaderLayoutGuide lub bottomTrailingButtonsLayoutGuide) i metody widoku akcesoriów nagłówka nie mają żadnego efektu. Zamiast tego zakotwicz widoki w widokach udostępnianych przez Google.

Właściwość compassButton obiektu GMSUISettings nadal kontroluje kompas, gdy interfejs nawigacji jest wyłączony. Gdy nawigacja jest włączona, Twój obiekt delegowany decyduje, czy wyświetlać kompas z tablicy buttons.

Lista kontrolna Essentials

Aby prawidłowo wdrożyć delegata układu, wykonaj te podstawowe czynności i spełnij te wymagania:

  • Ustaw przedstawiciela przed włączeniem nawigacji: wywołaj setLayoutDelegate(_:) w GMSMapView, zanim włączysz nawigację za pomocą isNavigationEnabled lub enableNavigation(with:). Metoda zgłasza błąd, jeśli nawigacja jest już włączona lub jeśli widok mapy został utworzony na ekranie CarPlay. Widok mapy zachowuje silne odwołanie do Twojego obiektu delegowanego. Aby przywrócić wbudowany układ, przekaż nil, gdy nawigacja jest wyłączona.

    // 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
        
  • Dołącz obowiązkowe widoki: we wszystkich stanach nawigacji musisz dodać do kontenera układu logo Google (googleLogo) i obszar wyświetlania (viewport).

  • Uwzględnianie stałych wymiarów: pakiet SDK określa wymiary kilku komponentów dostarczanych przez Google. Nie stosuj niestandardowych ograniczeń szerokości ani wysokości do tych elementów:

    • Karta nawigacji
    • Karta szacowanego czasu dotarcia na miejsce
    • Przychodzące prompty
    • Logo Google
    • Widżet prędkości
  • Dodaj elementy jako widoki podrzędne: dodaj widoki udostępnione przez Google bezpośrednio jako widoki podrzędne do niestandardowego widoku kontenera lub do samego elementu GMSMapView.

  • Wyrównywanie za pomocą prowadnic układu: widoki udostępniane przez Google zawierają wbudowane marginesy wizualne. Aby idealnie dopasować elementy niestandardowe do komponentów dostarczanych przez Google, ogranicz widoki do właściwości layoutMarginsGuide widoku Google.

  • Czyszczenie przy wyjściu ze stanu: w metodzie stopGuidance(navMap:newState:oldState:) usuń aktywne widoki wskazówek i dezaktywuj ich ograniczenia, zanim przywrócisz układ gotowy do nawigacji. W pliku disableNavigationUI(navMap:oldState:) usuń wszystkie dodane widoki i prowadnice układu, w tym wszelkie instrukcje na ekranie. Zapobiega to nakładaniu się widoków z jednego stanu na następny.

  • Własne ułatwienia dostępu i obsługa języków pisanych od prawej do lewej: widoki udostępniane przez Google spełniają standardy ułatwień dostępu, ale za ułatwienia dostępu w całym układzie odpowiada delegat. Aby obsługiwać języki pisane od prawej do lewej, używaj kotwic początkowych i końcowych zamiast lewych i prawych.

  • Unikaj starszych interfejsów API układu: nie używaj starszych właściwości modyfikujących układ w GMSUISettings ani starszych przewodników po układzie w GMSMapView, gdy aktywny jest niestandardowy delegat. Gdy dołączony jest niestandardowy obiekt delegowany, pakiet SDK może ignorować te połączenia starszego typu.

Stany interfejsu i komponenty Google

Gdy następuje zmiana stanu nawigacji, pakiet SDK przekazuje do wywołania zwrotnego obiektu delegowanego obiekt NavigationUIState tylko do odczytu. Ten obiekt łączy bieżące flagi konfiguracji układu z komponentami Google (np. instancjami UIView), które są potrzebne do renderowania ekranu.

Delegat układu zarządza hierarchią widoków w 4 stanach operacyjnych. Poniższy diagram stanów ilustruje, jak pakiet SDK przechodzi między stanami nawigacji i jakie metody wywołania zwrotnego wykonuje na delegacie:

Diagram przejścia stanu cyklu życia NavigationLayoutDelegate pokazujący przejścia między stanami Non-Navigation, Navigation Ready i Active Guidance.

Konkretne komponenty Google udostępniane obiektowi delegowanemu zależą od bieżącego etapu cyklu życia nawigacji.

 Wymagane komponenty (wszystkie stany nawigacji)

Niezależnie od bieżącego etapu nawigacji musisz uwzględnić, umieścić i utrzymać widoczność tych komponentów Google w hierarchii widoków:

  • Logo Google (googleLogo): ten komponent wyświetla obowiązkowe logo Map Google. Jeśli przycisk Wyśrodkuj jest włączony, logo automatycznie przekształca się w ten przycisk, gdy kierowca przewija mapę poza pojazd. W związku z tym umiejscowienie logo określa, gdzie pojawi się przycisk Wyśrodkuj. Zalecamy umieszczenie logo w lewym dolnym rogu układu, aby odpowiadało standardowym oczekiwaniom dotyczącym interfejsu mapy.

  • Widoczny obszar (viewport): niewidoczny element UIView, który określa granice kadrowania kamery. Umiejscowienie widocznego obszaru określa, gdzie pakiet SDK wyśrodkuje ikonę pojazdu i narysuje aktywną linię trasy. Ustaw widoczny obszar tak, aby obejmował otwarty, niezasłonięty obszar ekranu, bezpiecznie unikając nieprzezroczystych nakładek, takich jak niestandardowe plansze dolne.

W stanie gotowości do nawigacji interfejs pozostaje minimalistyczny, aby skupić uwagę na mapie podstawowej. Obiekt NavigationReadyUIState zapewnia dostęp do tych funkcji:

  • buttons: tablica pływających widoków działań skonfigurowanych dla mapy podstawowej (zwykle tylko przycisk kompasu). Google może dodawać przyciski lub zmieniać ich kolejność w przyszłych wersjach pakietu SDK, dlatego unikaj polegania na stałych indeksach tablicy. Zamiast tego przejdź przez tablicę, odczytaj typ każdego przycisku, porównując jego właściwość type z NavigationButtonKnownType.compass, i wyodrębnij widok fizyczny, wywołując jego właściwość view. Pamiętaj, że przyciski w tym zestawie różnią się od tych dostępnych podczas aktywnego prowadzenia.

 aktywne komponenty wskazówek

Gdy rozpocznie się szczegółowa nawigacja, pakiet SDK odblokuje pełny zestaw elementów sterujących nawigacją. Obiekt ActiveGuidanceUIState zapewnia dostęp do tych komponentów Google:

  • turnCard: główny baner nagłówka, który wyświetla wskazówki dotyczące nadchodzącego manewru, odległość i wskazówki dotyczące pasa ruchu. Umieść go u góry układu, aby utworzyć znajomą hierarchię nawigacji i zakotwiczyć wokół niego widoki niestandardowe.

  • etaCard: baner w stopce wyświetlający szacowany czas przybycia, pozostały czas podróży i pozostałą odległość do miejsca docelowego. Umieść go wzdłuż dolnej krawędzi ekranu lub zintegruj jego współrzędne z niestandardowymi arkuszami zarządzania zadaniami.

  • tripProgressBar: pionowy pasek postępu, który pokazuje kierowcy, jaką część trasy już pokonał. W przeciwieństwie do starszych układów, które ściśle wiązały ten element z krawędzią mapy, masz pełną swobodę zakotwiczenia go w dowolnym miejscu – na przykład wzdłuż bocznej krawędzi niestandardowego kontenera.

  • speedWidget: pływający element sterujący prędkościomierzem i opublikowanymi ograniczeniami prędkości. W zależności od ustawień interfejsu API (właściwości shouldDisplaySpeedometer i shouldDisplaySpeedLimit obiektu GMSMapView) oraz dostępności danych ten widok dynamicznie zmienia rozmiar między 4 stanami wizualnymi w czasie działania (nie wyświetla niczego, tylko bieżącą prędkość, tylko ograniczenie prędkości lub oba odczyty). Ponieważ widżet może zmieniać rozmiar bez powiadomienia, zawsze zakotwiczaj otaczające widoki za pomocą ograniczeń, aby układ automatycznie dostosowywał się do zmian i nie dochodziło do nakładania się elementów.

  • buttons: rozszerzona tablica pływających widoków działań dla aktywnego stanu wskazówek (zwykle obejmuje zarówno kompas, jak i przycisk zgłaszania zdarzeń). Podobnie jak w stanie gotowości do nawigacji, możesz zlokalizować i wyodrębnić poszczególne widoki przycisków, filtrując tablicę według type (.compass lub .reporting) i wyodrębniając widok fizyczny za pomocą właściwości view. Możesz je następnie umieścić niezależnie lub dodać do GMSNavigationHidingStackView. Ten widok stosu układa przyciski pionowo, a gdy zabraknie miejsca, ukrywa je, zaczynając od końca tablicy arrangedSubviews. Nie ustawiaj właściwości isHidden w widokach, którymi zarządza.
Wskazówka: sprawdzone metody dotyczące kadrowania. Widok karty skrętu ma stałą wysokość opartą na NavigationStyleValues.headerNominalHeight, która jest wystarczająca dla każdej treści karty skrętu. Aby zachować stabilne kadrowanie aparatu, ogranicz górną krawędź elementu viewport bezpośrednio do dolnej krawędzi elementu turnCard, jak pokazano w przykładowej implementacji.

 dynamiczne komponenty promptu

Podpowiedzi, takie jak alerty o incydentach lub ostrzeżenia o fotoradarach, są uruchamiane niezależnie podczas aktywnego prowadzenia.

Gdy prośba jest gotowa do wyświetlenia, pakiet SDK wywołuje wywołanie zwrotne showPrompt(navMap:newPrompt:) i przekazuje widok newPrompt bezpośrednio. Delegat odpowiada za płynne pozycjonowanie tej prośby nad układem (zwykle jest ona zakotwiczona na dolnej krawędzi kontenera mapy).

Ponieważ przychodzący komunikat zakrywa dolną część ekranu, musisz zaktualizować układ, aby komunikat nie nakładał się na widoczny obszar, logo Google ani żadne przyciski wyrównane do dołu.

Delegat określa też, jak prośby pojawiają się na ekranie i z niego znikają. Więcej informacji znajdziesz w artykule o dostosowywaniu promptów.

Obsługa rozmiaru ekranu i trybu szerokiego

Aby obsługiwać zmiany wymiarów mapy i orientacji ekranu, delegat układu korzysta z tych funkcji:

  • Zmiana rozmiaru widoku: dostosowuje układ, gdy zmienia się fizyczny rozmiar mapy.

  • Tryb szeroki: przełącza się na wariant układu w formacie szerokim, gdy mapa jest wystarczająco szeroka.

Reagowanie na zmianę rozmiaru widoku

Układy podzielonego ekranu, suwaki kontenerów i obracanie urządzenia często wywołują wywołanie zwrotne sizeChanged(navMap:newState:). Użyj tego wywołania zwrotnego, aby wprowadzić ogólne dostosowania niestandardowego interfejsu użytkownika do różnych urządzeń. Zaimplementuj sizeChanged(navMap:newState:), aby zmienić położenie elementów niestandardowych w przypadku nowego formatu obrazu, zastosować własne niestandardowe punkty przerwania szerokości lub wysokości i wykryć, kiedy zdarzenie zmiany rozmiaru przełącza tryb szeroki, aby bezpiecznie zamienić warianty układu.

Informacje o trybie szerokim

Tryb szeroki aktywuje się, gdy kontener mapy jest wystarczająco szeroki, aby wyświetlać komponenty interfejsu obok siebie.

Odczytaj wartość logiczną wideMode z obiektu stanu, aby zmienić pozycję niestandardowych elementów interfejsu i zapewnić kierowcy widoczność środka mapy. Komponenty dostarczane przez Google, takie jak karta skrętu i karta szacowanego czasu dotarcia na miejsce, automatycznie zmniejszają się i zmieniają kształt w trybie szerokim, dlatego odczytanie tej wartości logicznej zapewnia, że układ dostosuje się dokładnie w tym samym momencie, w którym aktualizują się komponenty Google.

Poniżej znajdziesz przykłady pozycjonowania komponentów układu w trybie standardowym i szerokim:

  • Standardowy tryb portretowy: karta skrętu znajduje się u góry ekranu, a karta szacowanego czasu dotarcia na miejsce – u dołu.

  • Tryb szeroki: karta skrętu i karta szacowanego czasu dotarcia na miejsce powinny znajdować się przy krawędzi ekranu, przy czym karta skrętu powinna być u góry, a karta szacowanego czasu dotarcia na miejsce – u dołu. Umieszczenie którejkolwiek karty z dala od krawędzi natarcia powoduje niezdefiniowane zachowanie. Możesz umieścić widoczny obszar i własne treści w dowolnym miejscu. Często wybiera się stronę końcową kart.

Sprawdzanie trybu szerokiego podczas zmian stanu

Jeśli układ obsługuje wariant trybu szerokiego, w każdym wywołaniu zwrotnym przejścia stanu oceniaj wartość logiczną wideMode, zamiast polegać tylko na sizeChanged(navMap:newState:).

Sprawdź obiekt stanu podczas wywołań zwrotnych, takich jak enableNavigationUI(navMap:newState:) i startGuidance(navMap:newState:oldState:), aby obsłużyć początkowe uruchomienia aplikacji w orientacji poziomej. Dzięki temu układ ocenia stan trybu szerokiego i aktywuje prawidłowy wariant układu standardowego lub szerokiego w momencie rozpoczęcia nowego stanu nawigacji.

Dostosowywanie do stylu Google

Aby ułatwić dopasowanie odstępów w widokach niestandardowych do komponentów dostarczanych przez Google, pakiet SDK udostępnia klasę NavigationStyleValues. Właściwości klasy zwracają pomiary w punktach.

Jeśli na przykład chcesz umieścić przycisk niestandardowy w górnym rogu ekranu naprzeciwko karty skrętu, jako górny margines przycisku użyj NavigationStyleValues.headerTopPadding. Przycisk zostanie wyrównany do górnej krawędzi karty skrętu.

Dostępne są te pomiary:

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

Widoki udostępniane przez Google zawierają też wbudowane marginesy. Gdy układasz 2 widoki dostarczone przez Google, ogranicz jeden do layoutMarginsGuide drugiego, aby ich marginesy się nie sumowały. Aby na przykład umieścić widżet prędkości nad kartą szacowanego czasu dotarcia na miejsce, ogranicz dolną kotwicę speedWidget do etaCard.layoutMarginsGuide.topAnchor.

Dostosowywanie promptów

Delegat układu określa, jak prompty pojawiają się na ekranie i z niego znikają oraz które ich rogi są zaokrąglone.

Animowane prompty

Pakiet SDK nie animuje próśb. Przedstawiciel decyduje, jak każdy prompt ma się pojawiać i znikać:

  • W programie showPrompt(navMap:newPrompt:) dodaj prompt do widoku mapy w pozycji początkowej, a następnie animuj go, aby przesunął się na właściwe miejsce. Możesz na przykład rozpocząć prośbę tuż pod widocznym obszarem lub za planszą dolną, wyrównując jej górną krawędź do górnej krawędzi planszy.
  • W metodzie hidePrompt(navMap:oldPrompt:) animuj prośbę, aby zniknęła z pola widzenia, a następnie usuń ją z widoku nadrzędnego po zakończeniu animacji.
  • W funkcji changePrompt(navMap:newPrompt:oldPrompt:) uruchom jednocześnie animacje ukrywania i pokazywania, aby zastąpić jeden prompt innym.

Takie podejście jest stosowane w przykładowej implementacji.

Aby wyłączyć prośby o informacje o natężeniu ruchu, lepszych trasach i zdarzeniach, ustaw właściwość shouldDisplayPrompts obiektu GMSNavigator na false.

Zaokrąglanie rogów prompta

Prompty są zgodne z protokołem NavigationRoundedCornerView, więc możesz wybrać, które ich rogi mają być zaokrąglone. Domyślnie zaokrąglone są tylko górne rogi, co pasuje do prośby zakotwiczonej przy dolnej krawędzi ekranu. Jeśli umieścisz prompt nad innymi treściami, np. planszą dolną, zaokrąglij wszystkie 4 rogi:

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

Przykład: implementacja układu opartego na ograniczeniach

Poniższy przykład pokazuje podstawowy delegat układu, który używa ograniczeń automatycznego układu do pozycjonowania karty skrętu, karty szacowanego czasu dotarcia na miejsce, przycisków, logo Google i widocznego obszaru. Możesz też przesuwać instrukcje na ekranie i poza niego. Aby przykład był krótki, nie obsługuje on trybu szerokiego, widżetu prędkości ani paska postępu przejazdu, więc nadaje się do aplikacji, które obsługują tylko orientację pionową.

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

Aby użyć obiektu delegowanego, ustaw go w widoku mapy przed włączeniem nawigacji, jak opisano na liście kontrolnej podstawowych czynności.

Ograniczenia i znane problemy

Dostosowywanie układu nie dotyczy CarPlay, który używa szablonów o stałym układzie. Jeśli wywołasz metodę setLayoutDelegate(_:) w widoku mapy utworzonym na potrzeby ekranu CarPlay, metoda zwróci błąd.

Problemy, które występują w konkretnych wersjach pakietu SDK, znajdziesz w sekcji Znane problemy w informacjach o wersji.