Navigationslayout anpassen

Beispielanimation, die ein benutzerdefiniertes Navigationslayout unter iOS zeigt

Hinweis: In diesem Dokument werden Swift-Namenskonventionen für Klassen, Protokolle und APIs verwendet.

Mit der Layoutanpassung können Sie die Ansichtshierarchie und die Bildschirmplatzierung sowohl für von Google bereitgestellte Ansichten als auch für Ihre eigenen benutzerdefinierten Geschäftsansichten steuern.

Anstatt benutzerdefinierte Ansichten in vordefinierte Layout-Slots einzufügen, implementieren Sie einen Layout-Delegaten, um das Layout für den gesamten Bildschirm zu verwalten. Bei Zustandsübergängen während der Navigation erhält Ihr Layout-Delegate die von Google bereitgestellten Komponenten, z. B. die Abbiegekarte, die voraussichtliche-Ankunft-Karte und die Schaltflächen. Diese Elemente können Sie mithilfe von standardmäßigen iOS-Auto-Layout-Einschränkungen neben Ihren eigenen benutzerdefinierten Inhalten positionieren.

Mit diesem Framework können Sie benutzerdefinierte Geschäftsinformationen wie den Bestellstatus oder Abholanweisungen genau auf dem Bildschirm positionieren und gleichzeitig verhindern, dass sich Ihre Ansichten überschneiden.

Funktionsweise der Layoutanpassung

Für die Layoutanpassung wird ein Delegationsentwurfsmuster verwendet. Anstatt UI-Komponenten automatisch auf dem Bildschirm zu zeichnen oder zu positionieren, übergibt das SDK sie direkt an eine von Ihnen geschriebene benutzerdefinierte Klasse: den Layout-Delegaten.

Wenn Sie das Layout anpassen möchten, erstellen Sie eine Klasse, die dem Protokoll NavigationLayoutDelegate entspricht, und übergeben Sie eine Instanz an die Methode setLayoutDelegate(_:) Ihres GMSMapView. Bei jedem Wechsel des Navigationsstatus, z. B. von der Basiskarte zur aktiven detaillierten Routenführung, führt das SDK die Callback-Methoden für Ihren Delegaten aus und stellt die UI-Komponenten bereit, die für diesen Status verfügbar sind.

Um eine nahtlose, integrierte Nutzererfahrung zu bieten, wird mit diesem Framework eine klare Trennung der Verantwortlichkeiten zwischen Ihrer App und dem SDK festgelegt. Mit dem Layout-Delegaten können Sie Folgendes anpassen:

  • Hierarchie der Ansichten erstellen: Wählen Sie genau aus, welche Google-Komponenten und benutzerdefinierten Geschäftsansichten für jeden UI-Status auf dem Bildschirm angezeigt werden sollen.
  • Jedes Element positionieren: Legen Sie genaue Bildschirmanker, Ränder und Layoutplatzierungen fest. Vermeiden Sie es, benutzerdefinierte Breiten- oder Höhenbeschränkungen auf Google-Komponenten anzuwenden, da Google-Komponenten ihre eigenen internen Abmessungen berechnen.
  • Basiskarte einrahmen: Definieren Sie die sichtbaren Grenzen der Kartenkamera mithilfe der Koordinaten der Darstellungsbereichskomponente.
  • Ebenen auf dem Bildschirm: Legen Sie fest, ob benutzerdefinierte Ansichten über, unter oder neben den integrierten Steuerelementen von Google angezeigt werden sollen.

Die folgenden Google-Komponenten können nicht mit dem Layout-Delegat angepasst werden:

  • Komponentenabmessungen: Die Größe und die internen Abmessungen der von Google bereitgestellten Komponenten, die vom SDK automatisch berechnet werden.
  • Auslösebedingungen: Wann dynamische Benachrichtigungen oder Aufforderungen basierend auf Echtzeit-Routendaten angezeigt werden.

 Implementierungsprinzipien

Beachten Sie beim Schreiben eines Layout-Delegaten die folgenden Regeln, um Layoutfehler oder Laufzeitabstürze zu vermeiden:

  • Standardmäßig wird ein leerer Bildschirm angezeigt: Von Google bereitgestellte Komponenten werden nur angezeigt, wenn sie von Ihrem Delegaten explizit der Ansichtshierarchie hinzugefügt und positioniert werden.
  • Legacy-APIs werden nicht unterstützt: Properties und Methoden, die das Legacy-Slot-basierte Layoutmodell steuern, werden nicht unterstützt und verhalten sich möglicherweise nicht wie erwartet, wenn ein benutzerdefinierter Delegat aktiv ist.
  • Interne Ansichtsstrukturen nicht ändern: Die Ansichtshierarchien von Google-Komponenten (z. B. Abbiege- oder voraussichtliche-Ankunft-Karte) dürfen nicht durchlaufen oder geändert werden. Da diese internen Ansichtshierarchien zugrunde liegende Implementierungsdetails sind, können sie sich bei SDK-Releases ändern. Wenn Sie sie ändern, kann es bei zukünftigen SDK-Updates zu Fehlern im Layout kommen.

-Kompatibilität mit Legacy-API

Wenn Sie einen benutzerdefinierten Layout-Delegaten verwenden, sollten Sie die folgenden alten slotbasierten APIs nicht verwenden, da sie eingestellt werden sollen. Wenn Sie eine vorhandene App migrieren möchten, ersetzen Sie die Verwendung dieser APIs durch Code in Ihrem benutzerdefinierten Layout-Delegat:

Slot-basierte Legacy-APIs anzeigen

Legacy API Layout-Delegate-Ersatz
isNavigationHeaderEnabled Lesen Sie die Ansicht turnCard in startGuidance(navMap:newState:oldState:).
isNavigationFooterEnabled Lesen Sie die Ansicht etaCard in startGuidance(navMap:newState:oldState:).
isNavigationTripProgressBarEnabled Lesen Sie die Ansicht tripProgressBar in startGuidance(navMap:newState:oldState:).
isNavigationReportIncidentButtonEnabled Suchen Sie im Array buttons nach dem Button .reporting.
setHeaderAccessoryView(_:) und invalidateLayout(forAccessoryView:) Füge dem Layout eine eigene Ansicht hinzu und verankere sie am turnCard.
padding und paddingAdjustmentBehavior Positioniere die viewport-Ansicht so, dass die Kamera im Bild ist.
navigationHeaderLayoutGuide Verankern Sie Ihre benutzerdefinierten Ansichten direkt im turnCard.
navigationFooterLayoutGuide Verankern Sie Ihre benutzerdefinierten Ansichten direkt im etaCard.
bottomTrailingButtonsLayoutGuide Verankern Sie Ihre Schaltflächen direkt an den etaCard- oder Container-Hilfslinien.
bottomTrailingButtonsAlternateLayoutGuide Verankern Sie Ihre Schaltflächen direkt an der Schaltfläche .reporting oder den Einschränkungen wideMode.
navigatorWillPresentPrompt(_:) und navigatorDidDismissPrompt(_:) Verlassen Sie sich auf die NavigationLayoutDelegate-Prompt-Callbacks, z. B. showPrompt(navMap:newPrompt:).

Wenn ein benutzerdefinierter Layout-Delegat festgelegt ist, haben die alten Layout-Guides (z. B. navigationHeaderLayoutGuide oder bottomTrailingButtonsLayoutGuide) und die Methoden für die Ansicht des Header-Zubehörs keine Auswirkungen. Verankern Sie Ihre Ansichten stattdessen in den von Google bereitgestellten Ansichten.

Das Attribut compassButton von GMSUISettings steuert den Kompass weiterhin, während die Navigations-UI deaktiviert ist. Wenn die Navigation aktiviert ist, entscheidet der Bevollmächtigte, ob der Kompass aus dem buttons-Array angezeigt werden soll.

Essentials-Checkliste

Führen Sie die folgenden wichtigen Schritte aus und beachten Sie die Anforderungen, um Ihren Layout-Delegaten erfolgreich zu implementieren:

  • Bevor Sie die Navigation aktivieren, müssen Sie den Bevollmächtigten festlegen: Rufen Sie setLayoutDelegate(_:) für GMSMapView auf, bevor Sie die Navigation mit isNavigationEnabled oder enableNavigation(with:) aktivieren. Die Methode gibt einen Fehler zurück, wenn die Navigation bereits aktiviert ist oder wenn Sie die Kartenansicht für einen CarPlay-Bildschirm erstellt haben. Die Kartenansicht behält einen starken Verweis auf Ihren Bevollmächtigten. Wenn Sie das integrierte Layout wiederherstellen möchten, übergeben Sie nil, während die Navigation deaktiviert ist.

    // 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
        
  • Obligatorische Ansichten anhängen: Sie müssen das Google-Logo (googleLogo) und den Viewport (viewport) in allen Navigationsstatus Ihrem Layoutcontainer hinzufügen.

  • Feste Abmessungen berücksichtigen: Das SDK bestimmt die Abmessungen für mehrere von Google bereitgestellte Komponenten. Wenden Sie keine benutzerdefinierten Breiten- oder Höhenbeschränkungen auf die folgenden Elemente an:

    • Abbiegekarte
    • voraussichtliche-Ankunft-Karte
    • Eingehende Aufforderungen
    • Google-Logo
    • Widget für die Geschwindigkeit
  • Elemente als Unteransichten hinzufügen: Fügen Sie die von Google bereitgestellten Ansichten direkt als Unteransichten zu Ihrer benutzerdefinierten Containeransicht oder zur GMSMapView selbst hinzu.

  • An Layout-Hilfslinien ausrichten: Die von Google bereitgestellten Ansichten enthalten integrierte visuelle Ränder. Wenn Sie Ihre benutzerdefinierten Elemente perfekt an den von Google bereitgestellten Komponenten ausrichten möchten, beschränken Sie Ihre Ansichten auf das Attribut layoutMarginsGuide der Google-Ansicht.

  • Beim Verlassen des Status bereinigen: Entfernen Sie in stopGuidance(navMap:newState:oldState:) die aktiven Zielführungsansichten und deaktivieren Sie ihre Einschränkungen, bevor Sie das Layout für die Navigation wiederherstellen. Entfernen Sie in disableNavigationUI(navMap:oldState:) alle hinzugefügten Ansichten und Layout-Guides, einschließlich aller Aufforderungen auf dem Bildschirm. Dadurch wird verhindert, dass sich Ansichten aus einem Zustand mit dem nächsten überschneiden.

  • Eigene Bedienungshilfen und Unterstützung für von rechts nach links geschriebene Sprachen: Die von Google bereitgestellten Ansichten entsprechen den Standards für Bedienungshilfen. Ihr Bevollmächtigter ist jedoch für die Bedienungshilfen des gesamten Layouts verantwortlich. Verwenden Sie zur Unterstützung von Sprachen, die von rechts nach links geschrieben werden, führende und nachgestellte Anker anstelle von linken und rechten Ankern.

  • Vermeide Legacy-Layout-APIs: Verwende keine Legacy-Eigenschaften zur Layoutänderung für GMSUISettings oder Legacy-Layout-Guides für GMSMapView, während ein benutzerdefiniertes Delegate aktiv ist. Das SDK ignoriert diese Legacy-Aufrufe möglicherweise, wenn ein benutzerdefinierter Delegate angehängt ist.

UI-Zustände und Google-Komponenten

Wenn ein Navigationsstatusübergang stattfindet, übergibt das SDK ein schreibgeschütztes NavigationUIState-Objekt an den Callback des Delegaten. Dieses Objekt bündelt die aktuellen Layoutkonfigurations-Flags mit den Google-Komponenten (z. B. UIView-Instanzen), die Sie zum Rendern des Bildschirms benötigen.

Der Layout-Delegate verwaltet die Ansichtshierarchie über vier Betriebszustände hinweg. Das folgende Zustandsdiagramm veranschaulicht, wie das SDK zwischen Navigationszuständen wechselt und welche Callback-Methoden es für Ihren Delegaten ausführt:

Diagramm zum Übergang zwischen den Lebenszyklusstatus von NavigationLayoutDelegate, das die Übergänge zwischen den Status „Keine Navigation“, „Navigation bereit“ und „Aktive Zielführung“ zeigt.

Welche Google-Komponenten Ihrem Delegaten zur Verfügung gestellt werden, hängt von der aktuellen Phase des Navigationslebenszyklus ab.

 Erforderliche Komponenten (alle Navigationsstatus)

Unabhängig von der aktuellen Navigationsphase müssen Sie die folgenden Google-Komponenten in Ihre Ansichtshierarchie einfügen, positionieren und sichtbar halten:

  • Google-Logo (googleLogo): Diese Komponente zeigt das obligatorische Google Maps-Logo an. Wenn die Schaltfläche Neu zentrieren aktiviert ist, wird das Logo automatisch in diese Schaltfläche umgewandelt, wenn der Fahrer die Karte von seinem Fahrzeug wegscrollt. Die Platzierung des Logos bestimmt also, wo die Schaltfläche Neu zentrieren angezeigt wird. Es empfiehlt sich, das Logo in der unteren linken Ecke des Layouts zu platzieren, um den Erwartungen an die Standard-Kartenoberfläche zu entsprechen.

  • Darstellungsbereich (viewport): Ein unsichtbares UIView, das die Grenzen des Kamera-Framings definiert. Die Platzierung des Darstellungsbereichs bestimmt genau, wo das SDK das Fahrzeugsymbol zentriert und die aktive Routenlinie zeichnet. Positionieren Sie den Darstellungsbereich so, dass er den offenen, nicht verdeckten Bereich des Bildschirms abdeckt. Vermeiden Sie dabei undurchsichtige Overlays wie benutzerdefinierte Ansichten am unteren Rand.

Im Navigationsmodus bleibt die Benutzeroberfläche minimalistisch, damit der Fokus auf der Basiskarte liegt. Das NavigationReadyUIState-Objekt bietet Zugriff auf Folgendes:

  • buttons: Ein Array von unverankerten Aktionsansichten, die für die Basiskarte konfiguriert sind (normalerweise nur die Kompass-Schaltfläche). Da Google in zukünftigen SDK-Releases möglicherweise Schaltflächen hinzufügt oder neu anordnet, sollten Sie sich nicht auf feste Array-Indexe verlassen. Stattdessen müssen Sie das Array durchlaufen, den Typ jeder Schaltfläche lesen, indem Sie die Eigenschaft type mit NavigationButtonKnownType.compass vergleichen, und die physische Ansicht extrahieren, indem Sie die Eigenschaft view aufrufen. Die genauen Schaltflächen in diesem Array unterscheiden sich von denen, die während der aktiven Zielführung verfügbar sind.

 aktive Anleitungskomponenten

Sobald die detaillierte Routenführung beginnt, werden im SDK alle Navigationssteuerelemente freigeschaltet. Das ActiveGuidanceUIState-Objekt bietet Zugriff auf die folgenden Google-Komponenten:

  • turnCard: Das primäre Headerbanner, das die nächsten Fahranweisungen, Entfernungsangaben und die Spurführung anzeigt. Platzieren Sie sie oben im Layout, um eine vertraute Navigationshierarchie zu schaffen und Ihre benutzerdefinierten Ansichten darum herum zu verankern.

  • etaCard: Das Banner in der Fußzeile, das die voraussichtliche Ankunftszeit, die verbleibende Fahrzeit und die verbleibende Entfernung zum Ziel anzeigt. Platzieren Sie es am unteren Rand des Bildschirms oder integrieren Sie die Koordinaten in Ihre benutzerdefinierten Tabellen für die Aufgabenverwaltung.

  • tripProgressBar: Eine vertikale Fortschrittsanzeige, die dem Fahrer zeigt, wie weit er auf der aktuellen Route bereits gefahren ist. Im Gegensatz zu älteren Layouts, bei denen die Position fest am Kartenrand verankert war, können Sie die Position jetzt frei wählen, z. B. an einer Seitenkante Ihres benutzerdefinierten Containers.

  • speedWidget: Ein schwebendes Steuerelement für den Tachometer und die zulässigen Höchstgeschwindigkeiten. Je nach API-Einstellungen (die Attribute shouldDisplaySpeedometer und shouldDisplaySpeedLimit von GMSMapView) und Datenverfügbarkeit wird die Größe dieser Ansicht zur Laufzeit dynamisch zwischen vier visuellen Zuständen angepasst (keine Anzeige, nur die aktuelle Geschwindigkeit, nur die Geschwindigkeitsbegrenzung oder beide Messwerte). Da das Widget ohne Vorankündigung zwischen diesen Größen wechseln kann, sollten Sie die umgebenden Ansichten immer mit Einschränkungen verankern, damit sich Ihr Layout automatisch anpasst und räumliche Überschneidungen vermieden werden.

  • buttons: Ein erweitertes Array von unverankerten Aktionsansichten für den aktiven Navigationsstatus (normalerweise mit dem Kompass und der Schaltfläche für Verkehrsmeldungen). Wie im navigationsbereiten Zustand können Sie einzelne Schaltflächenansichten finden und extrahieren, indem Sie das Array nach type (.compass oder .reporting) filtern und die physische Ansicht mithilfe der Eigenschaft view extrahieren. Sie können sie dann unabhängig voneinander positionieren oder einem GMSNavigationHidingStackView hinzufügen. In dieser Stack-Ansicht werden die Schaltflächen vertikal angeordnet. Wenn der Platz nicht ausreicht, werden sie ausgeblendet, beginnend am Ende des arrangedSubviews-Arrays. Legen Sie isHidden nicht für die von ihm verwalteten Ansichten fest.
Tipp: Best Practice für die Kameraausrichtung. Die Ansicht der Abbiegekarte hat eine feste Höhe, die auf NavigationStyleValues.headerNominalHeight basiert. Sie ist hoch genug für alle Inhalte der Abbiegekarte. Damit der Bildausschnitt der Kamera stabil bleibt, beschränke den oberen Rand von viewport direkt auf den unteren Rand von turnCard, wie in der Beispielimplementierung gezeigt.

 Komponenten für dynamische Prompts

Aufforderungen wie Unfallmeldungen oder Warnungen vor Blitzern werden während der aktiven Zielführung unabhängig voneinander ausgelöst.

Wenn eine Aufforderung angezeigt werden kann, ruft das SDK den Callback showPrompt(navMap:newPrompt:) des Bevollmächtigten auf und übergibt direkt eine newPrompt-Ansicht. Der Delegate ist dafür verantwortlich, diese Aufforderung reibungslos über dem Layout zu positionieren (normalerweise am unteren Rand des Kartencontainers verankert).

Da die eingehende Aufforderung den unteren Teil des Bildschirms abdeckt, müssen Sie Ihr Layout aktualisieren, um zu verhindern, dass die Aufforderung den Darstellungsbereich, das Google-Logo oder unten ausgerichtete Schaltflächen überlappt.

Der Delegate steuert auch, wie Prompts auf dem Bildschirm ein- und ausgeblendet werden. Weitere Informationen finden Sie unter Prompts anpassen.

Bildschirmgröße und Breitbildmodus

Der Layout-Delegate verwendet die folgenden Funktionen, um Änderungen an den Kartendimensionen und der Bildschirmausrichtung zu verarbeiten:

  • Größenanpassung der Ansicht: Das Layout wird angepasst, wenn sich die physische Größe der Karte ändert.

  • Breitbildmodus: Wenn die Karte breit genug ist, wird zu einer Layoutvariante im Breitbildformat gewechselt.

Auf Größenänderungen der Ansicht reagieren

Splitscreen-Layouts, Container-Schieberegler und das Drehen des Geräts lösen häufig den sizeChanged(navMap:newState:)-Callback aus. Verwenden Sie diesen Callback, um allgemeine responsive Anpassungen an Ihrer benutzerdefinierten Benutzeroberfläche vorzunehmen. Implementiere sizeChanged(navMap:newState:), um benutzerdefinierte Elemente für ein neues Seitenverhältnis des Bildschirms neu zu positionieren, eigene benutzerdefinierte Haltepunkte für Breite oder Höhe anzuwenden und zu erkennen, wenn ein Ereignis zur Größenänderung den Breitbildmodus umschaltet, um Layoutvarianten sicher zu wechseln.

Breitbildmodus

Der breite Modus wird aktiviert, wenn der Kartencontainer breit genug ist, um UI-Komponenten nebeneinander anzuzeigen.

Lesen Sie den booleschen Wert wideMode aus dem Zustandsobjekt, um Ihre benutzerdefinierten UI-Elemente neu zu positionieren und die Mitte der Karte für den Fahrer freizuhalten. Da von Google bereitgestellte Komponenten wie die Abbiegekarte und die voraussichtliche-Ankunft-Karte im breiten Modus automatisch verkleinert und umgeformt werden, sorgt das Lesen dieses booleschen Werts dafür, dass sich Ihr Layout genau im selben Moment anpasst, in dem die Google-Komponenten aktualisiert werden.

Sehen Sie sich die folgenden Beispiele für die Positionierung Ihrer Layoutkomponenten im Standard- und Breitbildmodus an:

  • Standard-Porträtmodus: Die Abbiegekarte wird oben auf dem Bildschirm und die voraussichtliche-Ankunft-Karte unten angezeigt.

  • Breitbildmodus: Die Abbiege- und die voraussichtliche-Ankunft-Karte werden am vorderen Rand des Bildschirms angezeigt, wobei die Abbiegekarte oben und die voraussichtliche-Ankunft-Karte unten ist. Wenn Sie eine der beiden Karten nicht an der Vorderkante platzieren, führt dies zu einem undefinierten Verhalten. Sie können den Darstellungsbereich und Ihre eigenen Inhalte an einer beliebigen Stelle platzieren. Eine gängige Wahl ist die nachfolgende Seite der Karten.

 Breitbildmodus bei Statusübergängen prüfen

Wenn dein Layout eine Variante für den breiten Modus unterstützt, solltest du den booleschen Wert wideMode in jedem Zustandsübergangs-Callback auswerten, anstatt dich nur auf sizeChanged(navMap:newState:) zu verlassen.

Prüfe das Zustandsobjekt während Callbacks wie enableNavigationUI(navMap:newState:) und startGuidance(navMap:newState:oldState:), um den ersten Start der App im Querformat zu verarbeiten. So wird sichergestellt, dass Ihr Layout den Status des breiten Modus auswertet und die richtige Standard- oder breite Layoutvariante aktiviert, sobald ein neuer Navigationsstatus beginnt.

An Google-Stil anpassen

Damit die Abstände Ihrer benutzerdefinierten Ansichten mit denen der von Google bereitgestellten Komponenten übereinstimmen, bietet das SDK die Klasse NavigationStyleValues. Die Klasseneigenschaften geben Maße in Punkten zurück.

Wenn Sie beispielsweise eine benutzerdefinierte Schaltfläche in der oberen Ecke des Bildschirms gegenüber der Abbiegekarte platzieren möchten, verwenden Sie NavigationStyleValues.headerTopPadding als oberen Rand der Schaltfläche. Die Schaltfläche wird dann am oberen Rand der Abbiegekarte ausgerichtet.

Folgende Messungen sind verfügbar:

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

Von Google bereitgestellte Ansichten enthalten auch integrierte Margen. Wenn Sie zwei von Google bereitgestellte Ansichten stapeln, beschränken Sie eine auf die layoutMarginsGuide der anderen, damit sich ihre Ränder nicht addieren. Wenn Sie beispielsweise das Geschwindigkeits-Widget über der voraussichtliche-Ankunft-Karte platzieren möchten, binden Sie den unteren Anker von speedWidget an etaCard.layoutMarginsGuide.topAnchor.

Prompts anpassen

Mit dem Layout-Delegaten können Sie festlegen, wie Prompts auf dem Bildschirm ein- und ausgeblendet werden und welche Ecken abgerundet sind.

Prompts animieren

Das SDK animiert keine Prompts für Sie. Der Delegate entscheidet, wie die einzelnen Aufforderungen angezeigt werden und verschwinden:

  • Fügen Sie in showPrompt(navMap:newPrompt:) die Aufforderung an einer Startposition zur Kartenansicht hinzu und animieren Sie sie dann an die gewünschte Position. Beginne die Aufforderung beispielsweise direkt unter dem sichtbaren Bereich oder hinter einer Ansicht am unteren Rand, wobei die Oberkante an der Oberkante der Ansicht ausgerichtet ist.
  • Animieren Sie in hidePrompt(navMap:oldPrompt:) die Aufforderung, sodass sie nicht mehr sichtbar ist, und entfernen Sie sie dann aus der Superview, wenn die Animation beendet ist.
  • Führen Sie in changePrompt(navMap:newPrompt:oldPrompt:) die Animationen zum Aus- und Einblenden gleichzeitig aus, um eine Aufforderung durch eine andere zu ersetzen.

In der Beispielimplementierung wird dieser Ansatz verwendet.

Wenn Sie Aufforderungen zu Verkehr, besseren Routen und Vorfällen deaktivieren möchten, setzen Sie das Attribut shouldDisplayPrompts von GMSNavigator auf false.

Ecken von Prompts abrunden

Prompts entsprechen dem NavigationRoundedCornerView-Protokoll, sodass Sie auswählen können, welche Ecken abgerundet werden sollen. Standardmäßig werden nur die oberen Ecken einer Aufforderung abgerundet. Das eignet sich für Aufforderungen, die am unteren Bildschirmrand verankert sind. Wenn du eine Aufforderung über anderen Inhalten platzierst, z. B. über einer Ansicht am unteren Rand, runde alle vier Ecken ab:

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

Beispiel: Implementierung eines einschränkungsbasierten Layouts

Das folgende Beispiel zeigt einen einfachen Layout-Delegaten, der Auto Layout-Einschränkungen verwendet, um die Abbiegekarte, die voraussichtliche-Ankunft-Karte, die Schaltflächen, das Google-Logo und den Darstellungsbereich zu positionieren. Außerdem werden Prompts auf dem Bildschirm ein- und ausgeblendet. Um das Beispiel kurz zu halten, werden der Breitbildmodus, das Geschwindigkeits-Widget und die Fortschrittsanzeige für die Fahrt nicht berücksichtigt. Es eignet sich daher für Apps, die nur das Hochformat unterstützen.

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

Wenn Sie den Bevollmächtigten verwenden möchten, müssen Sie ihn in der Kartenansicht festlegen, bevor Sie die Navigation aktivieren, wie in der Checkliste für die wichtigsten Schritte beschrieben.

Beschränkungen und bekannte Probleme

Die Layoutanpassung gilt nicht für CarPlay, da dort feste Layoutvorlagen verwendet werden. Wenn Sie setLayoutDelegate(_:) in einer Kartenansicht aufrufen, die Sie für einen CarPlay-Bildschirm erstellen, gibt die Methode einen Fehler zurück.

Informationen zu Problemen, die bestimmte SDK-Versionen betreffen, finden Sie in den Versionshinweisen unter Bekannte Probleme.