@iternio/react-native-auto-play 0.5.14-beta.1 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +270 -36
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
- package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +69 -15
- package/ios/Types.swift +3 -0
- package/ios/extensions/UIImage+Toggle.swift +5 -2
- package/ios/hybrid/HybridAutoPlay.swift +28 -16
- package/ios/hybrid/HybridMapTemplate.swift +22 -5
- package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
- package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
- package/ios/templates/AutoPlayTemplate.swift +24 -2
- package/ios/templates/GridTemplate.swift +57 -57
- package/ios/templates/InformationTemplate.swift +84 -9
- package/ios/templates/ListTemplate.swift +66 -11
- package/ios/templates/MapTemplate.swift +83 -68
- package/ios/templates/MessageTemplate.swift +72 -6
- package/ios/templates/Parser.swift +564 -69
- package/ios/templates/SearchTemplate.swift +1 -1
- package/ios/templates/TemplateStore.swift +5 -15
- package/ios/utils/SymbolFont.swift +13 -28
- package/jest.js +2 -0
- package/lib/Constants.d.ts +4 -0
- package/lib/Constants.js +6 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.web.d.ts +73 -0
- package/lib/index.web.js +72 -1
- package/lib/specs/MapTemplate.nitro.d.ts +3 -1
- package/lib/templates/GridTemplate.d.ts +23 -3
- package/lib/templates/GridTemplate.js +8 -0
- package/lib/templates/InformationTemplate.d.ts +38 -12
- package/lib/templates/ListTemplate.d.ts +16 -47
- package/lib/templates/MapTemplate.d.ts +26 -2
- package/lib/templates/MapTemplate.js +16 -2
- package/lib/templates/MessageTemplate.d.ts +52 -11
- package/lib/templates/MessageTemplate.js +1 -1
- package/lib/types/Image.d.ts +6 -1
- package/lib/utils/NitroColor.d.ts +1 -0
- package/lib/utils/NitroColor.js +8 -0
- package/lib/utils/NitroImage.js +1 -1
- package/lib/utils/NitroManeuver.js +1 -3
- package/lib/utils/NitroOptionsPanel.d.ts +103 -0
- package/lib/utils/NitroOptionsPanel.js +63 -0
- package/lib/utils/NitroSection.d.ts +89 -2
- package/lib/utils/NitroSection.js +49 -2
- package/lib-cjs/AutoPlayHeadlessJsTask.js +16 -0
- package/lib-cjs/Constants.js +9 -0
- package/lib-cjs/components/MapTemplateContext.js +10 -0
- package/lib-cjs/components/SafeAreaInsetsContext.js +21 -0
- package/lib-cjs/components/SafeAreaView.js +22 -0
- package/lib-cjs/components/WindowInformationWrapper.js +20 -0
- package/lib-cjs/hooks/useAndroidAutoTelemetry.js +115 -0
- package/lib-cjs/hooks/useFocusedEffect.js +35 -0
- package/lib-cjs/hooks/useMapTemplate.js +13 -0
- package/lib-cjs/hooks/useSafeAreaInsets.js +9 -0
- package/lib-cjs/hooks/useVoiceInput.js +42 -0
- package/lib-cjs/hybrid/HybridAndroidAutoTelemetry.js +4 -0
- package/lib-cjs/hybrid/HybridAndroidWindowInformation.js +4 -0
- package/lib-cjs/hybrid/HybridAutoPlay.js +5 -0
- package/lib-cjs/hybrid/HybridVoice.js +61 -0
- package/lib-cjs/index.js +76 -0
- package/lib-cjs/index.web.js +98 -0
- package/lib-cjs/scenes/AutoPlayCluster.js +140 -0
- package/lib-cjs/scenes/CarPlayDashboardScene.js +100 -0
- package/lib-cjs/specs/AndroidAutoTelemetry.nitro.js +2 -0
- package/lib-cjs/specs/AndroidAutomotive.nitro.js +16 -0
- package/lib-cjs/specs/AndroidWindowInformation.nitro.js +2 -0
- package/lib-cjs/specs/AutoPlay.nitro.js +2 -0
- package/lib-cjs/specs/CarPlayDashboard.nitro.js +2 -0
- package/lib-cjs/specs/Cluster.nitro.js +2 -0
- package/lib-cjs/specs/GridTemplate.nitro.js +2 -0
- package/lib-cjs/specs/InformationTemplate.nitro.js +2 -0
- package/lib-cjs/specs/ListTemplate.nitro.js +2 -0
- package/lib-cjs/specs/MapTemplate.nitro.js +2 -0
- package/lib-cjs/specs/MessageTemplate.nitro.js +2 -0
- package/lib-cjs/specs/SearchTemplate.nitro.js +2 -0
- package/lib-cjs/specs/SignInTemplate.nitro.js +2 -0
- package/lib-cjs/specs/Voice.nitro.js +2 -0
- package/lib-cjs/templates/GridTemplate.js +41 -0
- package/lib-cjs/templates/InformationTemplate.js +49 -0
- package/lib-cjs/templates/ListTemplate.js +33 -0
- package/lib-cjs/templates/MapTemplate.js +168 -0
- package/lib-cjs/templates/MessageTemplate.js +50 -0
- package/lib-cjs/templates/SearchTemplate.js +32 -0
- package/lib-cjs/templates/SignInTemplate.js +55 -0
- package/lib-cjs/templates/Template.js +40 -0
- package/lib-cjs/types/Button.js +12 -0
- package/lib-cjs/types/Event.js +2 -0
- package/lib-cjs/types/Image.js +2 -0
- package/lib-cjs/types/Maneuver.js +77 -0
- package/lib-cjs/types/RootComponent.js +2 -0
- package/lib-cjs/types/SignInMethod.js +22 -0
- package/lib-cjs/types/Telemetry.js +24 -0
- package/lib-cjs/types/Text.js +8 -0
- package/lib-cjs/types/Trip.js +2 -0
- package/lib-cjs/types/Voice.js +2 -0
- package/lib-cjs/utils/ErrorUtil.js +34 -0
- package/lib-cjs/utils/NitroAction.js +129 -0
- package/lib-cjs/utils/NitroAlert.js +19 -0
- package/lib-cjs/utils/NitroAttributedString.js +14 -0
- package/lib-cjs/utils/NitroColor.js +32 -0
- package/lib-cjs/utils/NitroGrid.js +12 -0
- package/lib-cjs/utils/NitroImage.js +97 -0
- package/lib-cjs/utils/NitroManeuver.js +79 -0
- package/lib-cjs/utils/NitroMapButton.js +33 -0
- package/lib-cjs/utils/NitroOptionsPanel.js +66 -0
- package/lib-cjs/utils/NitroSection.js +112 -0
- package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
- package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
- package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
- package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
- package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
- package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
- package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
- package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
- package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
- package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
- package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
- package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
- package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
- package/nitrogen/generated/android/c++/JNitroColor.hpp +8 -4
- package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
- package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
- package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
- package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
- package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
- package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroColor.kt +9 -4
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
- package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
- package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
- package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
- package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
- package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
- package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
- package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
- package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
- package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
- package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
- package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
- package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
- package/nitrogen/generated/ios/swift/NitroColor.swift +20 -2
- package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
- package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
- package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
- package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
- package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
- package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
- package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
- package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
- package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
- package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
- package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
- package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
- package/nitrogen/generated/shared/c++/NitroColor.hpp +7 -3
- package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
- package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
- package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
- package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
- package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
- package/package.json +7 -3
- package/src/Constants.ts +9 -0
- package/src/index.ts +1 -0
- package/src/index.web.ts +166 -0
- package/src/specs/MapTemplate.nitro.ts +3 -1
- package/src/templates/GridTemplate.ts +38 -3
- package/src/templates/InformationTemplate.ts +60 -29
- package/src/templates/ListTemplate.ts +27 -55
- package/src/templates/MapTemplate.ts +60 -2
- package/src/templates/MessageTemplate.ts +71 -24
- package/src/types/Image.ts +6 -1
- package/src/utils/NitroColor.ts +10 -1
- package/src/utils/NitroImage.ts +1 -5
- package/src/utils/NitroManeuver.ts +1 -4
- package/src/utils/NitroOptionsPanel.ts +218 -0
- package/src/utils/NitroSection.ts +160 -5
|
@@ -11,6 +11,31 @@ import CarPlay
|
|
|
11
11
|
class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
12
12
|
let interfaceController: CPInterfaceController
|
|
13
13
|
|
|
14
|
+
/// `CPMapTemplate` exposes no way to inspect its panel stack (only push/pop operations),
|
|
15
|
+
/// so this mirrors the combined logical navigation stack of regular pushed templates and
|
|
16
|
+
/// panels pushed onto a `CPMapTemplate`, in push order, to know what `popTemplate` etc.
|
|
17
|
+
/// need to pop next.
|
|
18
|
+
private enum NavigationEntry {
|
|
19
|
+
case template(id: String)
|
|
20
|
+
case panel(id: String, mapTemplate: CPMapTemplate)
|
|
21
|
+
|
|
22
|
+
var id: String {
|
|
23
|
+
switch self {
|
|
24
|
+
case .template(let id): return id
|
|
25
|
+
case .panel(let id, _): return id
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private var navigationStack: [NavigationEntry] = []
|
|
31
|
+
|
|
32
|
+
/// Set by `templateWillDisappear` whenever root is covered while a panel is visible, and
|
|
33
|
+
/// consulted by `templateWillAppear` to know which panel to reveal — inferring it from
|
|
34
|
+
/// `navigationStack` position only works when the coverer is a pushed template (tracked in
|
|
35
|
+
/// the stack); a presented template (e.g. a `MessageTemplate` without `mapConfig`) never
|
|
36
|
+
/// touches `navigationStack` at all, so position-based inference silently breaks for it.
|
|
37
|
+
private var coveredPanelId: String?
|
|
38
|
+
|
|
14
39
|
init(
|
|
15
40
|
interfaceController: CPInterfaceController
|
|
16
41
|
) {
|
|
@@ -29,18 +54,10 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
|
29
54
|
interfaceController.rootTemplate
|
|
30
55
|
}
|
|
31
56
|
|
|
32
|
-
var topTemplate: CPTemplate? {
|
|
33
|
-
interfaceController.topTemplate
|
|
34
|
-
}
|
|
35
|
-
|
|
36
57
|
var templates: [CPTemplate] {
|
|
37
58
|
interfaceController.templates
|
|
38
59
|
}
|
|
39
60
|
|
|
40
|
-
var topTemplateId: String? {
|
|
41
|
-
return interfaceController.topTemplate?.id
|
|
42
|
-
}
|
|
43
|
-
|
|
44
61
|
var rootTemplateId: String? {
|
|
45
62
|
return interfaceController.rootTemplate.id
|
|
46
63
|
}
|
|
@@ -49,99 +66,240 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
|
49
66
|
_ templateToPush: CPTemplate,
|
|
50
67
|
animated: Bool
|
|
51
68
|
) async throws -> Bool {
|
|
52
|
-
|
|
69
|
+
let result = try await interfaceController.pushTemplate(
|
|
53
70
|
templateToPush,
|
|
54
71
|
animated: animated
|
|
55
72
|
)
|
|
73
|
+
|
|
74
|
+
navigationStack.append(.template(id: templateToPush.id))
|
|
75
|
+
|
|
76
|
+
return result
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Pushes `panel` onto the current root template, which must already be a `CPMapTemplate`
|
|
80
|
+
/// (e.g. a `MapTemplate` previously set via `setRootTemplate`).
|
|
81
|
+
@available(iOS 27.0, *)
|
|
82
|
+
func pushPanel(
|
|
83
|
+
_ panel: CPMapPanel,
|
|
84
|
+
templateId: String
|
|
85
|
+
) async throws {
|
|
86
|
+
guard let mapTemplate = rootTemplate as? CPMapTemplate else {
|
|
87
|
+
throw AutoPlayError.rootTemplateNotMapTemplate(
|
|
88
|
+
"\(templateId) has mapConfig set, which requires the root template to be a CPMapTemplate"
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// A regular pushed template covers the map (and everything on it, panels included) on
|
|
93
|
+
// screen, but pushPanel only operates on the map's own, separate panel stack — pushing
|
|
94
|
+
// onto it here would silently attach to a hidden surface with no visible effect and no
|
|
95
|
+
// lifecycle callbacks, so this must be rejected rather than desyncing navigationStack.
|
|
96
|
+
guard
|
|
97
|
+
!navigationStack.contains(where: {
|
|
98
|
+
if case .template(let id) = $0 { return id != rootTemplate.id }
|
|
99
|
+
return false
|
|
100
|
+
})
|
|
101
|
+
else {
|
|
102
|
+
throw AutoPlayError.mapTemplateNotVisible(
|
|
103
|
+
"\(templateId) has mapConfig set, but the map template is currently covered by another pushed template"
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// CarPlay rejects pushPanel outright while a navigation alert is showing
|
|
108
|
+
// (CPMapTemplatePanelErrorDomain code 4) — dismiss it first, same as showAlert already
|
|
109
|
+
// does when replacing one alert with another.
|
|
110
|
+
if mapTemplate.currentNavigationAlert != nil {
|
|
111
|
+
await withCheckedContinuation { (continuation: CheckedContinuation<Void, Never>) in
|
|
112
|
+
mapTemplate.dismissNavigationAlert(animated: true) { _ in
|
|
113
|
+
continuation.resume()
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try await withCheckedThrowingContinuation {
|
|
119
|
+
(continuation: CheckedContinuation<Void, Error>) in
|
|
120
|
+
mapTemplate.pushPanel(panel) { _, error in
|
|
121
|
+
if let error {
|
|
122
|
+
continuation.resume(throwing: error)
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
continuation.resume()
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
navigationStack.append(.panel(id: templateId, mapTemplate: mapTemplate))
|
|
56
130
|
}
|
|
57
131
|
|
|
58
132
|
func setRootTemplate(
|
|
59
133
|
_ rootTemplate: CPTemplate,
|
|
60
134
|
animated: Bool
|
|
61
135
|
) async throws -> Bool {
|
|
62
|
-
|
|
136
|
+
let result = try await interfaceController.setRootTemplate(
|
|
63
137
|
rootTemplate,
|
|
64
138
|
animated: animated
|
|
65
139
|
)
|
|
140
|
+
|
|
141
|
+
let previousEntries = navigationStack
|
|
142
|
+
navigationStack = [.template(id: rootTemplate.id)]
|
|
143
|
+
|
|
144
|
+
// No CarPlay callback tears down a panel when its root is replaced, unlike templateDidDisappear for regular templates.
|
|
145
|
+
if #available(iOS 27.0, *) {
|
|
146
|
+
let panelIds = previousEntries.compactMap { entry -> String? in
|
|
147
|
+
if case .panel(let id, _) = entry { return id }
|
|
148
|
+
return nil
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if !panelIds.isEmpty {
|
|
152
|
+
// Only the topmost entry could still be visible; others already got their disappear pair when covered.
|
|
153
|
+
if let last = previousEntries.last, case .panel(let visiblePanelId, _) = last {
|
|
154
|
+
try? RootModule.withAutoPlayTemplate(templateId: visiblePanelId) {
|
|
155
|
+
(template: AutoPlayTemplate) in
|
|
156
|
+
template.onWillDisappear(animated: animated)
|
|
157
|
+
template.onDidDisappear(animated: animated)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try RootModule.withTemplateStore { templateStore in
|
|
162
|
+
templateStore.removeTemplates(templateIds: panelIds)
|
|
163
|
+
}
|
|
164
|
+
panelIds.forEach { HybridAutoPlay.removeListeners(templateId: $0) }
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return result
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Whether `templateId` is the top entry of the combined template/panel navigation stack.
|
|
172
|
+
func isTopEntry(templateId: String) -> Bool {
|
|
173
|
+
return navigationStack.last?.id == templateId
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Removes the navigation-stack entry for `templateId` if present; idempotent if it's
|
|
177
|
+
/// already gone. For `.template` entries, `popTopEntry` removes the entry directly for
|
|
178
|
+
/// app-initiated pops, and `templateDidDisappear` calls this for CarPlay-initiated pops
|
|
179
|
+
/// (e.g. its built-in back button) that `popTopEntry` never sees. For `.panel` entries,
|
|
180
|
+
/// `popTopEntry` doesn't touch `navigationStack` at all — `CPMapPanelDelegate.panelDidHide`
|
|
181
|
+
/// calling this is the only place a panel entry is removed, covering both app-initiated
|
|
182
|
+
/// and CarPlay-initiated pops alike.
|
|
183
|
+
func removeNavigationEntryIfPresent(templateId: String) {
|
|
184
|
+
navigationStack.removeAll { $0.id == templateId }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/// Ids of all panel entries currently in the navigation stack, in push order. `navigationStack`
|
|
188
|
+
/// itself is private, so this is the minimal data `CPMapPanelDelegate` callbacks need to
|
|
189
|
+
/// simulate cover/reveal lifecycle events themselves — `CPMapPanelDelegate` has no
|
|
190
|
+
/// equivalent of `CPInterfaceControllerDelegate` telling a covered/revealed panel about it.
|
|
191
|
+
@available(iOS 27.0, *)
|
|
192
|
+
var panelTemplateIds: [String] {
|
|
193
|
+
navigationStack.compactMap { entry in
|
|
194
|
+
if case .panel(let id, _) = entry { return id }
|
|
195
|
+
return nil
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Whether any panel exists in the stack, even if currently covered by a regular template.
|
|
200
|
+
@available(iOS 27.0, *)
|
|
201
|
+
var hasPanel: Bool {
|
|
202
|
+
!panelTemplateIds.isEmpty
|
|
66
203
|
}
|
|
67
204
|
|
|
68
205
|
func popTemplate(
|
|
69
206
|
animated: Bool
|
|
70
207
|
) async throws -> String? {
|
|
71
|
-
|
|
208
|
+
return try await popTopEntry(animated: animated)
|
|
209
|
+
}
|
|
72
210
|
|
|
73
|
-
|
|
74
|
-
|
|
211
|
+
/// Pops whatever is on top of the combined navigation stack: a regular pushed template via
|
|
212
|
+
/// `CPInterfaceController.popTemplate`, or a panel via `CPMapTemplate.popPanelWithCompletion`.
|
|
213
|
+
private func popTopEntry(animated: Bool) async throws -> String? {
|
|
214
|
+
// Ensure at least one entry (the root) remains
|
|
215
|
+
guard navigationStack.count > 1, let top = navigationStack.last else { return nil }
|
|
75
216
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
217
|
+
switch top {
|
|
218
|
+
case .template(let templateId):
|
|
219
|
+
try await interfaceController.popTemplate(animated: animated)
|
|
79
220
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
221
|
+
try RootModule.withTemplateStore { templateStore in
|
|
222
|
+
templateStore.removeTemplate(templateId: templateId)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
removeNavigationEntryIfPresent(templateId: templateId)
|
|
226
|
+
|
|
227
|
+
return templateId
|
|
83
228
|
|
|
84
|
-
|
|
229
|
+
case .panel(let templateId, let mapTemplate):
|
|
230
|
+
guard #available(iOS 27.0, *) else { return nil }
|
|
231
|
+
|
|
232
|
+
try await mapTemplate.popPanel()
|
|
233
|
+
|
|
234
|
+
// popPanel() never triggers panelDidHide (only hidePanel() does), so this has to
|
|
235
|
+
// do the teardown + reveal itself rather than rely on the delegate.
|
|
236
|
+
await handlePanelPopped(templateId: templateId, animated: animated)
|
|
237
|
+
|
|
238
|
+
return templateId
|
|
239
|
+
}
|
|
85
240
|
}
|
|
86
241
|
|
|
87
242
|
func popToRootTemplate(
|
|
88
243
|
animated: Bool
|
|
89
244
|
) async throws -> [String] {
|
|
90
|
-
|
|
245
|
+
guard navigationStack.count > 1 else { return [] }
|
|
91
246
|
|
|
92
|
-
|
|
93
|
-
|
|
247
|
+
let entriesToPop = Array(navigationStack.dropFirst())
|
|
248
|
+
let poppedIds = entriesToPop.map { $0.id }
|
|
94
249
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
250
|
+
if entriesToPop.contains(where: {
|
|
251
|
+
if case .template = $0 { return true }
|
|
252
|
+
return false
|
|
253
|
+
}) {
|
|
254
|
+
try await interfaceController.popToRootTemplate(animated: animated)
|
|
99
255
|
}
|
|
100
256
|
|
|
101
|
-
|
|
102
|
-
return
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
257
|
+
let panelTemplates = entriesToPop.compactMap({ entry in
|
|
258
|
+
if case .panel = entry { return entry.id }
|
|
259
|
+
return nil
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
// The visible panel is left for panelDidHide to tear down (via hidePanel() below) so its
|
|
263
|
+
// lifecycle + button reclaim isn't duplicated here; only the already-covered ones (which
|
|
264
|
+
// already got their disappear pair) need removing now.
|
|
265
|
+
let visiblePanelId = panelTemplates.last
|
|
266
|
+
let idsToRemoveNow = poppedIds.filter { $0 != visiblePanelId }
|
|
107
267
|
|
|
108
268
|
try RootModule.withTemplateStore { templateStore in
|
|
109
|
-
templateStore.removeTemplates(templateIds:
|
|
269
|
+
templateStore.removeTemplates(templateIds: idsToRemoveNow)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Keep the visible panel tracked so panelDidHide still finds it there.
|
|
273
|
+
let keptEntry = visiblePanelId.flatMap { id in entriesToPop.first { $0.id == id } }
|
|
274
|
+
navigationStack = [navigationStack[0]] + (keptEntry.map { [$0] } ?? [])
|
|
275
|
+
|
|
276
|
+
if visiblePanelId != nil, #available(iOS 27.0, *) {
|
|
277
|
+
try await RootModule.withInterfaceController { interfaceController in
|
|
278
|
+
let mapTemplate = interfaceController.rootTemplate as? CPMapTemplate
|
|
279
|
+
try await mapTemplate?.hidePanel()
|
|
280
|
+
}
|
|
110
281
|
}
|
|
111
282
|
|
|
112
|
-
return
|
|
283
|
+
return poppedIds
|
|
113
284
|
}
|
|
114
285
|
|
|
115
286
|
func popToTemplate(templateId: String, animated: Bool) async throws
|
|
116
287
|
-> [String]
|
|
117
288
|
{
|
|
118
|
-
guard
|
|
119
|
-
|
|
120
|
-
where: {
|
|
121
|
-
templateId == $0.id
|
|
122
|
-
})
|
|
123
|
-
else { return [] }
|
|
124
|
-
|
|
125
|
-
var templateIds: [String] = interfaceController.templates.map {
|
|
126
|
-
template in template.id
|
|
289
|
+
guard navigationStack.contains(where: { $0.id == templateId }) else {
|
|
290
|
+
return []
|
|
127
291
|
}
|
|
128
292
|
|
|
129
|
-
|
|
130
|
-
$0 == templateId
|
|
131
|
-
}),
|
|
132
|
-
let endIndex = templateIds.firstIndex(where: {
|
|
133
|
-
$0 == topTemplateId
|
|
134
|
-
})
|
|
135
|
-
{
|
|
136
|
-
templateIds = Array(templateIds[(startIndex)..<endIndex])
|
|
137
|
-
}
|
|
293
|
+
var poppedIds: [String] = []
|
|
138
294
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
295
|
+
while navigationStack.last?.id != templateId {
|
|
296
|
+
guard let poppedId = try await popTopEntry(animated: animated) else {
|
|
297
|
+
break
|
|
298
|
+
}
|
|
299
|
+
poppedIds.append(poppedId)
|
|
300
|
+
}
|
|
143
301
|
|
|
144
|
-
return
|
|
302
|
+
return poppedIds
|
|
145
303
|
}
|
|
146
304
|
|
|
147
305
|
func presentTemplate(
|
|
@@ -181,6 +339,21 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
|
181
339
|
animated: animated
|
|
182
340
|
)
|
|
183
341
|
}
|
|
342
|
+
|
|
343
|
+
// Panels only attach to root, so only relevant when root reappears. `coveredPanelId` is
|
|
344
|
+
// set by templateWillDisappear when root was covered — this can't be inferred from
|
|
345
|
+
// navigationStack position, since a presented (not pushed) covering template, like a
|
|
346
|
+
// MessageTemplate without mapConfig, never touches navigationStack at all.
|
|
347
|
+
if #available(iOS 27.0, *), templateId == rootTemplateId {
|
|
348
|
+
if let panelId = coveredPanelId, panelTemplateIds.contains(panelId) {
|
|
349
|
+
try? RootModule.withAutoPlayTemplate(templateId: panelId) {
|
|
350
|
+
(template: AutoPlayTemplate) in
|
|
351
|
+
template.onWillAppear(animated: animated)
|
|
352
|
+
template.onDidAppear(animated: animated)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
coveredPanelId = nil
|
|
356
|
+
}
|
|
184
357
|
}
|
|
185
358
|
|
|
186
359
|
func templateDidAppear(
|
|
@@ -189,13 +362,6 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
|
189
362
|
) {
|
|
190
363
|
let templateId = aTemplate.id
|
|
191
364
|
|
|
192
|
-
if rootTemplateId == templateId {
|
|
193
|
-
// this makes sure we purge outdated CPSearchTemplate since that one can be popped on with a CarPlay native button we can not intercept
|
|
194
|
-
try? RootModule.withTemplateStore { templateStore in
|
|
195
|
-
templateStore.purge()
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
365
|
try? RootModule.withAutoPlayTemplate(
|
|
200
366
|
templateId: templateId,
|
|
201
367
|
perform: { (template: AutoPlayTemplate) in
|
|
@@ -222,6 +388,22 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
|
222
388
|
)
|
|
223
389
|
}
|
|
224
390
|
)
|
|
391
|
+
|
|
392
|
+
// Panels only attach to root, so only relevant when root is covered — by a push (not yet
|
|
393
|
+
// appended to navigationStack at this point, so .last is still the panel being covered)
|
|
394
|
+
// or by presenting a template (e.g. a MessageTemplate without mapConfig, which never
|
|
395
|
+
// touches navigationStack at all, so .last is still the panel either way).
|
|
396
|
+
if #available(iOS 27.0, *), templateId == rootTemplateId,
|
|
397
|
+
let last = navigationStack.last,
|
|
398
|
+
case .panel(let panelId, _) = last
|
|
399
|
+
{
|
|
400
|
+
coveredPanelId = panelId
|
|
401
|
+
try? RootModule.withAutoPlayTemplate(templateId: panelId) {
|
|
402
|
+
(template: AutoPlayTemplate) in
|
|
403
|
+
template.onWillDisappear(animated: animated)
|
|
404
|
+
template.onDidDisappear(animated: animated)
|
|
405
|
+
}
|
|
406
|
+
}
|
|
225
407
|
}
|
|
226
408
|
|
|
227
409
|
func templateDidDisappear(
|
|
@@ -239,14 +421,21 @@ class AutoPlayInterfaceController: NSObject, CPInterfaceControllerDelegate {
|
|
|
239
421
|
}
|
|
240
422
|
)
|
|
241
423
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
424
|
+
/// this makes sure onPopped for templates is only invoked when they are gone forever
|
|
425
|
+
/// in case the template is just hidden by some other template it will not fire
|
|
426
|
+
/// since CPAlertTemplate is presented and nothing can be pushed on top of it there is no special handling required
|
|
427
|
+
guard !interfaceController.templates.contains(where: { $0.id == templateId }) else {
|
|
428
|
+
return
|
|
429
|
+
}
|
|
246
430
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
431
|
+
removeNavigationEntryIfPresent(templateId: templateId)
|
|
432
|
+
|
|
433
|
+
try? RootModule.withTemplateStore { templateStore in
|
|
434
|
+
templateStore.removeTemplate(templateId: templateId)
|
|
250
435
|
}
|
|
436
|
+
|
|
437
|
+
HybridAutoPlay.removeListeners(
|
|
438
|
+
templateId: templateId
|
|
439
|
+
)
|
|
251
440
|
}
|
|
252
441
|
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
//
|
|
2
|
+
// AutoPlayMapPanelDelegate.swift
|
|
3
|
+
// Pods
|
|
4
|
+
//
|
|
5
|
+
|
|
6
|
+
import CarPlay
|
|
7
|
+
|
|
8
|
+
/// Shared `CPMapPanel.Delegate` for any `AutoPlayTemplate` that renders as a map panel
|
|
9
|
+
/// (`ListTemplate`, and future panel-backed templates like a grid). `CPMapPanel.delegate` is
|
|
10
|
+
/// `weak`, so the owning template must keep a strong reference to this instance for as long as
|
|
11
|
+
/// the panel exists.
|
|
12
|
+
@available(iOS 27.0, *)
|
|
13
|
+
class AutoPlayMapPanelDelegate: NSObject, CPMapPanel.Delegate {
|
|
14
|
+
private weak var template: AutoPlayTemplate?
|
|
15
|
+
private let templateId: String
|
|
16
|
+
// As of iOS 27 beta 4, CarPlay invokes panelDidShow twice for a single push. Guard against
|
|
17
|
+
// firing the JS appear callbacks more than once per panel.
|
|
18
|
+
private var hasFiredPanelDidShow = false
|
|
19
|
+
|
|
20
|
+
init(template: AutoPlayTemplate, templateId: String) {
|
|
21
|
+
self.template = template
|
|
22
|
+
self.templateId = templateId
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// There's no "will appear" delegate method, so `onWillAppear` and `onDidAppear` fire
|
|
26
|
+
/// together here. Also fires `onWillDisappear` and `onDidDisappear` on whichever panel was on top
|
|
27
|
+
/// before this one, since CPMapPanelDelegate never notifies a covered panel itself —
|
|
28
|
+
/// `panelDidShow` only tells us about the panel that just appeared.
|
|
29
|
+
func panelDidShow(_ panel: CPMapPanel) {
|
|
30
|
+
if hasFiredPanelDidShow {
|
|
31
|
+
return
|
|
32
|
+
}
|
|
33
|
+
hasFiredPanelDidShow = true
|
|
34
|
+
|
|
35
|
+
template?.onWillAppear(animated: true)
|
|
36
|
+
template?.onDidAppear(animated: true)
|
|
37
|
+
|
|
38
|
+
let templateId = self.templateId
|
|
39
|
+
|
|
40
|
+
Task { @MainActor in
|
|
41
|
+
var previousPanelId: String?
|
|
42
|
+
var mapTemplate: CPMapTemplate?
|
|
43
|
+
|
|
44
|
+
try? await RootModule.withInterfaceController { interfaceController in
|
|
45
|
+
mapTemplate = interfaceController.rootTemplate as? CPMapTemplate
|
|
46
|
+
previousPanelId =
|
|
47
|
+
interfaceController.panelTemplateIds
|
|
48
|
+
.filter { $0 != templateId }
|
|
49
|
+
.last
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// This panel owns the map template's bar buttons and map buttons while it's on top.
|
|
53
|
+
if let mapTemplate {
|
|
54
|
+
// Auto-hiding the bar shifts the panel and interrupts taps on it.
|
|
55
|
+
mapTemplate.automaticallyHidesNavigationBar = false
|
|
56
|
+
mapTemplate.hidesButtonsWithNavigationBar = false
|
|
57
|
+
|
|
58
|
+
applyPanelHeaderActions(self.template?.getPanelHeaderActions(), to: mapTemplate)
|
|
59
|
+
applyPanelMapButtons(self.template?.getPanelMapButtons(), to: mapTemplate)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
guard let previousPanelId else { return }
|
|
63
|
+
|
|
64
|
+
try? RootModule.withAutoPlayTemplate(templateId: previousPanelId) {
|
|
65
|
+
(template: AutoPlayTemplate) in
|
|
66
|
+
// let the now covered panel know it disappeared
|
|
67
|
+
template.onWillDisappear(animated: true)
|
|
68
|
+
template.onDidDisappear(animated: true)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// There's no "will disappear" delegate method, so `onWillDisappear` and `onDidDisappear`
|
|
74
|
+
/// fire together here. Also fires `onWillAppear` and `onDidAppear` on whichever panel is now on
|
|
75
|
+
/// top after this one is removed, since CPMapPanelDelegate never notifies a revealed panel
|
|
76
|
+
/// itself — `panelDidHide` only tells us about the panel that just disappeared.
|
|
77
|
+
/// https://developer.apple.com/documentation/ios-ipados-release-notes/ios-ipados-27-release-notes#CarPlay
|
|
78
|
+
func panelDidHide(_ panel: CPMapPanel) {
|
|
79
|
+
let templateId = self.templateId
|
|
80
|
+
|
|
81
|
+
Task { @MainActor in
|
|
82
|
+
var stillTracked = false
|
|
83
|
+
|
|
84
|
+
try? await RootModule.withInterfaceController { interfaceController in
|
|
85
|
+
stillTracked = interfaceController.panelTemplateIds.contains(templateId)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Already torn down elsewhere, e.g. by popToRootTemplate for a covered panel.
|
|
89
|
+
guard stillTracked else { return }
|
|
90
|
+
|
|
91
|
+
await handlePanelPopped(templateId: templateId, animated: true)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Tears down `templateId`'s panel and reveals whatever's now on top (or lets the map template
|
|
97
|
+
/// reclaim its own bar/map buttons if nothing remains). Shared by `panelDidHide` — which only
|
|
98
|
+
/// fires for a `hidePanel()`-triggered close — and `popTopEntry`'s `.panel` case, since
|
|
99
|
+
/// `popPanel()` never triggers `panelDidHide` and so has to run this itself.
|
|
100
|
+
@available(iOS 27.0, *)
|
|
101
|
+
@MainActor
|
|
102
|
+
func handlePanelPopped(templateId: String, animated: Bool) async {
|
|
103
|
+
var revealedPanelId: String?
|
|
104
|
+
var mapTemplate: CPMapTemplate?
|
|
105
|
+
|
|
106
|
+
try? await RootModule.withInterfaceController { interfaceController in
|
|
107
|
+
interfaceController.removeNavigationEntryIfPresent(templateId: templateId)
|
|
108
|
+
revealedPanelId = interfaceController.panelTemplateIds.last
|
|
109
|
+
mapTemplate = interfaceController.rootTemplate as? CPMapTemplate
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
try? RootModule.withAutoPlayTemplate(templateId: templateId) {
|
|
113
|
+
(template: AutoPlayTemplate) in
|
|
114
|
+
template.onWillDisappear(animated: animated)
|
|
115
|
+
template.onDidDisappear(animated: animated)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try? RootModule.withTemplateStore { templateStore in
|
|
119
|
+
templateStore.removeTemplate(templateId: templateId)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
HybridAutoPlay.removeListeners(templateId: templateId)
|
|
123
|
+
|
|
124
|
+
if let revealedPanelId {
|
|
125
|
+
// Another panel is now on top: it reclaims the bar/map buttons and its own appear pair, same as a fresh push.
|
|
126
|
+
try? RootModule.withAutoPlayTemplate(templateId: revealedPanelId) {
|
|
127
|
+
(revealed: AutoPlayTemplate) in
|
|
128
|
+
if let mapTemplate {
|
|
129
|
+
applyPanelHeaderActions(revealed.getPanelHeaderActions(), to: mapTemplate)
|
|
130
|
+
applyPanelMapButtons(revealed.getPanelMapButtons(), to: mapTemplate)
|
|
131
|
+
}
|
|
132
|
+
// let the now uncovered panel know it appeared again
|
|
133
|
+
revealed.onWillAppear(animated: animated)
|
|
134
|
+
revealed.onDidAppear(animated: animated)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
else if let mapTemplate {
|
|
138
|
+
// No panel remains: let the map template reclaim its own bar/map buttons.
|
|
139
|
+
try? RootModule.withAutoPlayTemplate(templateId: mapTemplate.id) {
|
|
140
|
+
(root: AutoPlayTemplate) in
|
|
141
|
+
root.invalidate()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Re-applies `templateId`'s header/map buttons to the root map template's nav bar, but only if
|
|
147
|
+
/// it's currently the topmost (visible) panel. `_invalidate()` (e.g. from `setHeaderActions()`)
|
|
148
|
+
/// runs for covered panels too, and only the topmost panel owns the nav bar — applying
|
|
149
|
+
/// unconditionally would clobber whichever panel is actually on top. A covered panel's own
|
|
150
|
+
/// buttons get reapplied later, when it's revealed again (see `handlePanelPopped`).
|
|
151
|
+
@available(iOS 27.0, *)
|
|
152
|
+
@MainActor
|
|
153
|
+
func reapplyPanelBarIfVisible(_ template: AutoPlayTemplate, templateId: String) {
|
|
154
|
+
guard
|
|
155
|
+
let scene = SceneStore.getRootScene(),
|
|
156
|
+
let interfaceController = scene.interfaceController,
|
|
157
|
+
interfaceController.panelTemplateIds.last == templateId,
|
|
158
|
+
let mapTemplate = interfaceController.rootTemplate as? CPMapTemplate
|
|
159
|
+
else { return }
|
|
160
|
+
|
|
161
|
+
applyPanelHeaderActions(template.getPanelHeaderActions(), to: mapTemplate)
|
|
162
|
+
applyPanelMapButtons(template.getPanelMapButtons(), to: mapTemplate)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// apply header buttons according to mapConfig.headerActions or remove the map provided buttons in case none are specified for the panel
|
|
166
|
+
@available(iOS 27.0, *)
|
|
167
|
+
@MainActor
|
|
168
|
+
private func applyPanelHeaderActions(_ headerActions: [NitroAction]?, to mapTemplate: CPMapTemplate) {
|
|
169
|
+
guard let headerActions else {
|
|
170
|
+
mapTemplate.backButton = nil
|
|
171
|
+
mapTemplate.leadingNavigationBarButtons = []
|
|
172
|
+
mapTemplate.trailingNavigationBarButtons = []
|
|
173
|
+
return
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
setBarButtons(template: mapTemplate, barButtons: headerActions)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// Applies mapConfig.mapButtons to the map template, or clears them if the panel specifies none.
|
|
180
|
+
@available(iOS 27.0, *)
|
|
181
|
+
@MainActor
|
|
182
|
+
private func applyPanelMapButtons(_ mapButtons: [NitroMapButton]?, to mapTemplate: CPMapTemplate) {
|
|
183
|
+
// Only need the root MapTemplate for its onPanButtonPress callback, since a .pan button still controls the same underlying map.
|
|
184
|
+
try? RootModule.withAutoPlayTemplate(templateId: mapTemplate.id) { (root: MapTemplate) in
|
|
185
|
+
mapTemplate.mapButtons = Parser.parseMapButtons(
|
|
186
|
+
mapButtons: mapButtons ?? [],
|
|
187
|
+
onPanButtonPress: root.onPanButtonPress
|
|
188
|
+
)
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -10,10 +10,30 @@ import CarPlay
|
|
|
10
10
|
class AutoPlayTemplate: NSObject {
|
|
11
11
|
public private(set) var autoDismissMs: Double?
|
|
12
12
|
|
|
13
|
-
func getTemplate() -> CPTemplate {
|
|
13
|
+
func getTemplate() throws -> CPTemplate {
|
|
14
14
|
fatalError("getTemplate not implemented")
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/// Templates that render as an overlay panel on an existing root `CPMapTemplate`
|
|
18
|
+
/// (iOS 27+, e.g. `ListTemplate` with `mapConfig` set) override this to return their
|
|
19
|
+
/// `CPMapPanel` instead of providing a `CPTemplate` via `getTemplate()`. Returned as `Any`
|
|
20
|
+
/// since `CPMapPanel` is only available on iOS 27+ while this class supports iOS 15.1+.
|
|
21
|
+
func getPanel() -> Any? {
|
|
22
|
+
return nil
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Header actions to apply to the root map template's bar while this template renders as
|
|
26
|
+
/// a map panel (see `getPanel()`) — distinct from any `barButtons` used for a plain
|
|
27
|
+
/// `CPTemplate`, since the panel itself has no nav bar of its own.
|
|
28
|
+
func getPanelHeaderActions() -> [NitroAction]? {
|
|
29
|
+
return nil
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Map buttons to apply to the root map template while this renders as a map panel (see `getPanel()`).
|
|
33
|
+
func getPanelMapButtons() -> [NitroMapButton]? {
|
|
34
|
+
return nil
|
|
35
|
+
}
|
|
36
|
+
|
|
17
37
|
@MainActor final func invalidate() {
|
|
18
38
|
if SceneStore.getRootScene() == nil {
|
|
19
39
|
return
|
|
@@ -25,7 +45,9 @@ class AutoPlayTemplate: NSObject {
|
|
|
25
45
|
/// Override in subclasses to perform template invalidation.
|
|
26
46
|
/// Do not call this method directly. Call `invalidate()` instead.
|
|
27
47
|
@MainActor func _invalidate() {}
|
|
28
|
-
@MainActor func traitCollectionDidChange() {
|
|
48
|
+
@MainActor func traitCollectionDidChange() {
|
|
49
|
+
invalidate()
|
|
50
|
+
}
|
|
29
51
|
|
|
30
52
|
func onWillAppear(animated: Bool) {}
|
|
31
53
|
func onDidAppear(animated: Bool) {}
|