
레이아웃 맞춤설정을 사용하여 Google에서 제공하는 뷰와 자체 커스텀 비즈니스 뷰의 뷰 계층 구조와 화면 배치를 제어합니다.
사전 정의된 레이아웃 슬롯에 맞춤 뷰를 추가하는 대신 레이아웃 대리자를 구현하여 전체 화면의 레이아웃을 관리합니다. 내비게이션 상태 전환 중에 레이아웃 대리자는 Google에서 제공하는 구성요소(예: 경로 카드, 도착 예정 시간 카드, 버튼)를 수신합니다. 표준 iOS 자동 레이아웃 제약 조건을 사용하여 이러한 요소를 자체 맞춤 콘텐츠와 함께 배치합니다.
이 프레임워크를 사용하면 주문 상태나 수령 안내와 같은 맞춤 비즈니스 정보를 화면에 정확하게 배치하면서 뷰가 겹치지 않도록 할 수 있습니다.
레이아웃 맞춤설정의 작동 방식
레이아웃 맞춤설정은 위임 디자인 패턴을 사용합니다. SDK는 화면에 UI 구성요소를 자동으로 그리거나 배치하는 대신, 개발자가 작성하는 맞춤 클래스인 레이아웃 대리자에 직접 전달합니다.
레이아웃을 맞춤설정하려면NavigationLayoutDelegate 프로토콜을 준수하는 클래스를 만들고 인스턴스를 GMSMapView의 setLayoutDelegate(_:) 메서드에 전달합니다. 기본 지도에서 활성 세부 경로 안내로 이동하는 등 내비게이션 상태가 전환될 때마다 SDK는 대리자에서 콜백 메서드를 실행하고 해당 특정 상태에 사용할 수 있는 UI 구성요소를 제공합니다.
원활한 통합 사용자 환경을 제공하기 위해 이 프레임워크는 앱과 SDK 간의 명확한 책임 분리를 설정합니다. 레이아웃 대리자를 사용하여 다음을 맞춤설정합니다.
- 뷰 계층 구조 빌드: 각 UI 상태의 화면에 추가할 Google 구성요소와 맞춤 비즈니스 뷰를 정확하게 선택합니다.
- 모든 요소 배치: 정확한 화면 앵커, 여백, 레이아웃 배치를 설정합니다. Google 구성요소는 자체 내부 크기를 계산하므로 Google 구성요소에 맞춤 너비 또는 높이 제약 조건을 적용하지 마세요.
- 기본 지도 프레임: 표시 영역 구성요소의 좌표를 사용하여 지도 카메라의 표시되는 경계를 정의합니다.
- 화면 레이어링: 커스텀 뷰가 Google의 기본 제공 컨트롤 위, 아래 또는 옆에 표시되는지 결정합니다.
한편 레이아웃 대리자를 사용하여 다음 Google 구성요소를 맞춤설정할 수 없습니다.
- 구성요소 크기: Google에서 제공하는 구성요소의 크기 및 내부 크기이며, SDK에서 자동으로 계산합니다.
- 트리거 조건: 실시간 경로 데이터를 기반으로 동적 알림 또는 메시지가 표시되는 경우입니다.
구현 원칙
레이아웃 대리자를 작성할 때는 레이아웃 버그나 런타임 비정상 종료를 방지하기 위해 다음 규칙에 유의하세요.
- 기본값은 빈 화면입니다. Google에서 제공하는 구성요소는 대리자가 뷰 계층 구조에 명시적으로 추가하고 배치하는 경우에만 표시됩니다.
- 기존 API는 지원되지 않음: 기존 슬롯 기반 레이아웃 모델을 제어하는 속성과 메서드는 지원되지 않으며 맞춤 위임이 활성 상태일 때 예상대로 작동하지 않을 수 있습니다.
- 내부 뷰 구조를 수정하지 마세요. Google에서 제공하는 구성요소 (예: 회전 카드 또는 ETA 카드)의 뷰 계층 구조를 순회하거나 수정하지 마세요. 이러한 내부 뷰 계층 구조는 기본 구현 세부정보이므로 SDK 출시 버전에 따라 변경될 수 있습니다. 수정하면 향후 SDK 업데이트 중에 레이아웃이 손상될 수 있습니다.
기존 API 호환성
맞춤 레이아웃 대리자를 사용할 때 안정적인 레이아웃 동작을 위해 지원 중단 예정인 다음 기존 슬롯 기반 API를 사용하지 마세요. 기존 앱을 이전하려면 이러한 API 사용을 맞춤 레이아웃 대리자의 코드로 대체하세요.
기존 슬롯 기반 API 표시
| Legacy API | 레이아웃 대리자 교체 |
|---|---|
isNavigationHeaderEnabled |
startGuidance(navMap:newState:oldState:)에서 turnCard 뷰를 읽습니다. |
isNavigationFooterEnabled |
startGuidance(navMap:newState:oldState:)에서 etaCard 뷰를 읽습니다. |
isNavigationTripProgressBarEnabled |
startGuidance(navMap:newState:oldState:)에서 tripProgressBar 뷰를 읽습니다. |
isNavigationReportIncidentButtonEnabled |
buttons 배열에서 .reporting 버튼을 찾습니다. |
setHeaderAccessoryView(_:) 및
invalidateLayout(forAccessoryView:) |
레이아웃에 자체 뷰를 추가하고 turnCard에 고정합니다. |
padding 및 paddingAdjustmentBehavior |
viewport 뷰를 배치하여 카메라를 프레이밍합니다. |
navigationHeaderLayoutGuide |
맞춤 뷰를 turnCard에 직접 고정합니다. |
navigationFooterLayoutGuide |
맞춤 뷰를 etaCard에 직접 고정합니다. |
bottomTrailingButtonsLayoutGuide |
버튼을 etaCard 또는 컨테이너 가이드에 직접 고정합니다. |
bottomTrailingButtonsAlternateLayoutGuide |
버튼을 .reporting 버튼 또는 wideMode 제약 조건에 직접 고정합니다. |
navigatorWillPresentPrompt(_:) 및
navigatorDidDismissPrompt(_:) |
showPrompt(navMap:newPrompt:)와 같은 NavigationLayoutDelegate 프롬프트 콜백을 사용합니다. |
커스텀 레이아웃 대리자가 설정되어 있는 동안에는 기존 레이아웃 가이드 (예: navigationHeaderLayoutGuide 또는 bottomTrailingButtonsLayoutGuide) 및 헤더 액세서리 뷰 메서드가 적용되지 않습니다. 대신 Google에서 제공하는 뷰에 뷰를 고정합니다.
내비게이션 UI가 사용 중지된 동안에도 GMSUISettings의 compassButton 속성은 계속 나침반을 제어합니다. 내비게이션이 사용 설정되어 있는 동안 대리자는 buttons 배열에서 나침반을 표시할지 결정합니다.
Essentials 체크리스트
레이아웃 대리자를 성공적으로 구현하려면 다음 필수 단계와 요구사항을 따르세요.
-
내비게이션을 사용 설정하기 전에 대리자 설정:
isNavigationEnabled또는enableNavigation(with:)를 사용하여 내비게이션을 사용 설정하기 전에GMSMapView에서setLayoutDelegate(_:)를 호출합니다. 내비게이션이 이미 사용 설정되어 있거나 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에서 제공하는 구성요소와 완벽하게 정렬하려면 뷰를 Google 뷰의
layoutMarginsGuide속성으로 제한하세요. -
상태 종료 시 정리:
stopGuidance(navMap:newState:oldState:)에서 활성화된 안내 뷰를 제거하고 내비게이션 준비 레이아웃을 복원하기 전에 해당 제약 조건을 비활성화합니다.disableNavigationUI(navMap:oldState:)에서 화면의 프롬프트를 포함하여 추가한 모든 뷰와 레이아웃 가이드를 삭제합니다. 이렇게 하면 한 상태의 뷰가 다음 상태의 뷰와 겹치지 않습니다. -
접근성 및 오른쪽에서 왼쪽으로 쓰기 지원: Google에서 제공하는 뷰는 접근성 표준을 충족하지만 대리자는 전체 레이아웃의 접근성을 책임집니다. 오른쪽에서 왼쪽으로 쓰는 언어를 지원하려면 왼쪽 및 오른쪽 앵커 대신 선행 및 후행 앵커를 사용하세요.
-
기존 레이아웃 API 피하기: 맞춤 대리자가 활성 상태인 동안
GMSUISettings에서 기존 레이아웃 수정 속성을 사용하거나GMSMapView에서 기존 레이아웃 가이드를 사용하지 마세요. 커스텀 대리자가 연결되면 SDK에서 이러한 기존 통화를 무시할 수 있습니다.
UI 상태 및 Google 구성요소
내비게이션 상태 전환이 발생하면 SDK는 읽기 전용 NavigationUIState 객체를 대리자 콜백에 전달합니다. 이 객체는 현재 레이아웃 구성 플래그를 화면을 렌더링하는 데 필요한 Google 구성요소 (예: UIView 인스턴스)와 번들로 묶습니다.
레이아웃 대리자는 네 가지 운영 상태에서 뷰 계층 구조를 관리합니다. 다음 상태 다이어그램은 SDK가 탐색 상태 간에 전환되는 방식과 대리자에서 실행되는 콜백 메서드를 보여줍니다.
대리자에게 제공되는 특정 Google 구성요소는 내비게이션 수명 주기의 현재 단계에 따라 다릅니다.
필수 구성요소 (모든 탐색 상태)
현재 탐색 단계와 관계없이 뷰 계층 구조에 다음 Google 구성요소를 포함하고 배치하고 표시해야 합니다.
Google 로고 (
googleLogo): 이 구성요소는 필수 Google 지도 로고를 표시합니다. 중심 재설정 버튼이 사용 설정된 경우 운전자가 지도를 스크롤하여 차량에서 멀어지면 로고가 자동으로 이 버튼으로 변환됩니다. 따라서 로고의 위치에 따라 중심 재설정 버튼이 표시되는 위치가 결정됩니다. 표준 지도 UI 기대치에 맞게 레이아웃의 하단 선행 모서리에 로고를 배치하는 것이 좋습니다.표시 영역 (
viewport): 카메라의 프레임 경계를 정의하는 보이지 않는UIView입니다. 표시 영역의 배치는 SDK가 차량 갈매기형 아이콘을 중앙에 배치하고 활성 경로 선을 그리는 위치를 정확하게 지정합니다. 뷰포트를 화면의 가려지지 않은 열린 영역을 덮도록 배치하여 맞춤 하단 시트와 같은 불투명 오버레이를 안전하게 피합니다.
탐색 지원 구성요소
내비게이션 준비 상태에서는 기본 지도에 집중할 수 있도록 UI가 최소한으로 유지됩니다. NavigationReadyUIState 객체는 다음에 대한 액세스 권한을 제공합니다.
- buttons: 기본 지도에 구성된 플로팅 작업 뷰의 배열입니다 (일반적으로 나침반 버튼만 해당). 향후 SDK 출시에서 Google이 버튼을 추가하거나
재정렬할 수 있으므로 고정 배열 색인에
의존하지 마세요. 대신 배열을 반복하고
type속성을NavigationButtonKnownType.compass과 비교하여 각 버튼의 유형을 읽고view속성을 호출하여 실제 뷰를 추출합니다. 이 배열의 정확한 버튼은 활성 안내 중에 사용할 수 있는 버튼과 다릅니다.
활성 가이드 구성요소 개
세부 경로 안내가 시작되면 SDK가 전체 내비게이션 컨트롤 모음을 잠금 해제합니다. ActiveGuidanceUIState 객체는 다음 Google 구성요소에 대한 액세스 권한을 제공합니다.
turnCard: 예정된 이동 방향, 거리 측정, 차선 안내를 표시하는 기본 헤더 배너입니다. 레이아웃 상단에 배치하여 익숙한 탐색 계층 구조를 설정하고 이를 중심으로 맞춤 뷰를 고정하세요.etaCard: 예상 도착 시간, 남은 이동 시간, 목적지까지 남은 거리를 표시하는 바닥글 배너입니다. 화면 하단 가장자리에 배치하거나 좌표를 맞춤 작업 관리 시트와 통합할 수 있습니다.tripProgressBar: 운전자가 현재 경로를 따라 이동한 거리를 보여주는 세로 진행률 표시줄입니다. 이를 지도 가장자리에 엄격하게 고정하는 기존 레이아웃과 달리, 커스텀 컨테이너의 측면 가장자리 등 원하는 위치에 자유롭게 고정할 수 있습니다.speedWidget: 속도계 및 게시된 속도 제한을 위한 플로팅 컨트롤입니다. API 설정(GMSMapView의shouldDisplaySpeedometer및shouldDisplaySpeedLimit속성)과 데이터 가용성에 따라 이 뷰는 런타임 시 네 가지 시각적 상태 (아무것도 표시하지 않음, 현재 속도만 표시, 제한 속도만 표시, 두 수치 모두 표시) 간에 크기가 동적으로 조절됩니다. 위젯은 예고 없이 이러한 크기 간에 전환될 수 있으므로 항상 제약 조건을 사용하여 주변 뷰를 고정하여 레이아웃이 자동으로 조정되어 공간 중복을 방지합니다.
buttons: 활성 경로 안내 상태의 확장된 플로팅 작업 뷰 배열입니다 (일반적으로 나침반과 교통 상황 제보 버튼이 모두 포함됨). 내비게이션 준비 상태와 마찬가지로type(.compass또는.reporting)로 배열을 필터링하고view속성을 사용하여 실제 뷰를 추출하여 개별 버튼 뷰를 찾고 추출할 수 있습니다. 그런 다음 독립적으로 배치하거나GMSNavigationHidingStackView에 추가할 수 있습니다. 이 스택 뷰는 버튼을 세로로 정렬하고 공간이 부족하면arrangedSubviews배열의 끝에서부터 버튼을 숨깁니다. 관리하는 뷰에isHidden를 설정하지 마세요.
NavigationStyleValues.headerNominalHeight에 따라 고정된 높이를 가지며, 이는 모든 회전 카드 콘텐츠에 충분한 높이입니다. 카메라 프레임을 안정적으로 유지하려면 구현 예와 같이 viewport의 상단을 turnCard의 하단 가장자리로 직접 제한합니다.
동적 프롬프트 구성요소
사고 알림 또는 안전 카메라 경고와 같은 메시지는 활성 안내 중에 독립적으로 트리거됩니다.
메시지를 표시할 준비가 되면 SDK는 대리자의 showPrompt(navMap:newPrompt:) 콜백을 호출하고 newPrompt 뷰를 직접 전달합니다. 대리자는 이 메시지를 레이아웃 위에 원활하게 배치해야 합니다 (일반적으로 지도 컨테이너의 하단 가장자리에 고정됨).
수신 프롬프트가 화면 하단을 가리므로 프롬프트가 표시 영역, Google 로고 또는 하단 정렬 버튼과 겹치지 않도록 레이아웃을 업데이트해야 합니다.
또한 대리자는 프롬프트가 화면에 표시되고 사라지는 방식을 제어합니다. 자세한 내용은 프롬프트 맞춤설정을 참조하세요.
화면 크기 및 와이드 모드 처리
지도 크기 및 화면 방향의 변경사항을 처리하기 위해 레이아웃 대리자는 다음 기능을 사용합니다.
뷰 크기 조절: 지도의 실제 크기가 변경될 때마다 레이아웃을 조정합니다.
와이드 모드: 지도가 충분히 넓으면 와이드 형식 레이아웃 변형으로 전환합니다.
뷰 크기 조절에 응답
분할 화면 레이아웃, 컨테이너 슬라이더, 기기 회전은 일반적으로sizeChanged(navMap:newState:) 콜백을 트리거합니다. 이 콜백을 사용하여 커스텀 UI에 일반적인 반응형 조정을 적용합니다. sizeChanged(navMap:newState:)를 구현하여 새 화면 가로세로 비율에 맞게 맞춤 요소를 재배치하고, 자체 맞춤 너비 또는 높이 중단점을 적용하고, 크기 조절 이벤트가 와이드 모드를 전환하여 레이아웃 변형을 안전하게 교체하는 시점을 감지합니다.
와이드 모드 이해하기
와이드 모드는 지도 컨테이너가 UI 구성요소를 나란히 표시할 수 있을 만큼 넓을 때 활성화됩니다.
상태 객체에서 wideMode 불리언을 읽어 맞춤 UI 요소의 위치를 변경하고 운전자를 위해 지도 중앙을 비워 둡니다. 회전 카드 및 도착 예정 시간 카드와 같은 Google 제공 구성요소는 와이드 모드에서 자동으로 축소되고 모양이 변경되므로 이 불리언을 읽으면 Google 구성요소가 업데이트되는 정확한 순간에 레이아웃이 조정됩니다.
표준 모드와 와이드 모드에서 레이아웃 구성요소를 배치하는 다음 예를 참고하세요.
표준 세로 모드: 회전 카드를 화면 상단에, 도착 예정 시간 카드를 하단에 배치합니다.
와이드 모드: 회전 카드와 ETA 카드를 화면의 선행 가장자리를 따라 유지합니다. 회전 카드는 상단에, ETA 카드는 하단에 배치합니다. 카드를 선행 에지에서 멀리 배치하면 정의되지 않은 동작이 발생합니다. 표시 영역과 자체 콘텐츠를 어디에나 배치할 수 있습니다. 일반적인 선택은 카드의 후행 측면입니다.
상태 전환 중 와이드 모드 확인
레이아웃이 와이드 모드 변형을 지원하는 경우 sizeChanged(navMap:newState:)에만 의존하지 말고 모든 상태 전환 콜백 내에서 wideMode 불리언을 평가합니다.
enableNavigationUI(navMap:newState:) 및 startGuidance(navMap:newState:oldState:)와 같은 콜백 중에 상태 객체를 확인하여 가로 모드 방향으로 초기 앱 실행을 처리합니다. 이 접근 방식을 사용하면 레이아웃이 와이드 모드 상태를 평가하고 새 탐색 상태가 시작되는 순간 올바른 표준 또는 와이드 레이아웃 변형을 활성화할 수 있습니다.
Google 스타일링에 맞춤
커스텀 뷰가 Google에서 제공하는 구성요소의 간격과 일치하도록 SDK는 NavigationStyleValues 클래스를 제공합니다. 클래스 속성은 측정값을 포인트 단위로 반환합니다.
예를 들어 회전 카드의 반대쪽 화면 상단 모서리에 맞춤 버튼을 배치하려면 NavigationStyleValues.headerTopPadding를 버튼의 상단 여백으로 사용합니다. 그러면 버튼이 회전 카드의 상단 가장자리에 맞춰집니다.
사용 가능한 측정값은 다음과 같습니다.
NavigationStyleValues.buttonHorizontalPaddingNavigationStyleValues.buttonVerticalPaddingNavigationStyleValues.defaultCornerRadiusNavigationStyleValues.elementHorizontalPaddingNavigationStyleValues.headerFooterHorizontalPaddingNavigationStyleValues.headerNominalHeightNavigationStyleValues.headerTopPadding
Google에서 제공하는 뷰에는 기본 제공 여백도 포함됩니다. Google에서 제공하는 뷰 두 개를 쌓을 때는 여백이 합산되지 않도록 하나를 다른 하나의 layoutMarginsGuide로 제한합니다. 예를 들어 속도 위젯을 ETA 카드 위에 배치하려면 speedWidget의 하단 앵커를 etaCard.layoutMarginsGuide.topAnchor로 제한합니다.
프롬프트 맞춤설정
레이아웃 대리자는 프롬프트가 화면에 표시되고 사라지는 방식과 모서리 중 어느 부분이 둥글게 처리되는지를 제어합니다.
애니메이션 프롬프트
SDK는 프롬프트에 애니메이션을 적용하지 않습니다. 대리자는 각 프롬프트가 표시되고 사라지는 방식을 결정합니다.
showPrompt(navMap:newPrompt:)에서 시작 위치의 지도 뷰에 프롬프트를 추가한 다음 애니메이션을 적용합니다. 예를 들어 표시 영역 바로 아래 또는 상단 가장자리가 시트 상단에 정렬된 하단 시트 뒤에서 프롬프트를 시작합니다.hidePrompt(navMap:oldPrompt:)에서 프롬프트가 보이지 않도록 애니메이션을 적용한 다음 애니메이션이 완료되면 상위 뷰에서 프롬프트를 삭제합니다.changePrompt(navMap:newPrompt:oldPrompt:)에서 숨기기 및 표시 애니메이션을 동시에 실행하여 한 프롬프트를 다른 프롬프트로 바꿉니다.
구현 예시에서는 이 접근 방식을 사용합니다.
교통정보, 더 나은 경로, 사고에 대한 메시지를 사용 중지하려면 GMSNavigator의 shouldDisplayPrompts 속성을 false로 설정합니다.
프롬프트 모서리 둥글게 처리
프롬프트는 NavigationRoundedCornerView 프로토콜을 준수하므로 모서리 중 어느 것을 둥글게 할지 선택할 수 있습니다. 기본적으로 프롬프트는 상단 모서리만 둥글게 처리하므로 화면 하단 가장자리에 고정된 프롬프트에 적합합니다.
하단 시트와 같은 다른 콘텐츠 위에 프롬프트를 배치하는 경우 네 모서리를 모두 둥글게 처리합니다.
newPrompt.roundedCorners = [
.layerMinXMinYCorner, .layerMaxXMinYCorner,
.layerMinXMaxYCorner, .layerMaxXMaxYCorner,
]
예: 제약 조건 기반 레이아웃 구현
다음 예는 자동 레이아웃 제약 조건을 사용하여 회전 카드, 도착 예정 시간 카드, 버튼, 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에는 적용되지 않습니다. CarPlay 화면용으로 만든 지도 뷰에서 setLayoutDelegate(_:)를 호출하면 메서드에서 오류가 발생합니다.
특정 SDK 버전에 영향을 미치는 문제는 출시 노트의 알려진 문제를 참고하세요.