@iternio/react-native-auto-play 0.5.14-beta.1 → 0.6.0
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 +214 -29
- 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 +21 -2
- 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/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/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/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/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++/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/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/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++/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 +1 -1
- package/src/Constants.ts +9 -0
- package/src/index.ts +1 -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/utils/NitroOptionsPanel.ts +218 -0
- package/src/utils/NitroSection.ts +160 -5
package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt
CHANGED
|
@@ -52,6 +52,7 @@ import com.margelo.nitro.swe.iternio.reactnativeautoplay.ForkType
|
|
|
52
52
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.GlyphImage
|
|
53
53
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.KeepType
|
|
54
54
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.ListTemplateConfig
|
|
55
|
+
import com.margelo.nitro.swe.iternio.reactnativeautoplay.ListImageType
|
|
55
56
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.ManeuverType
|
|
56
57
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroAction
|
|
57
58
|
import com.margelo.nitro.swe.iternio.reactnativeautoplay.NitroActionType
|
|
@@ -84,6 +85,10 @@ import kotlin.math.abs
|
|
|
84
85
|
import androidx.core.net.toUri
|
|
85
86
|
|
|
86
87
|
object Parser {
|
|
88
|
+
// IMAGE_TYPE_MEDIUM was added to androidx.car.app in 1.8.0. Keep the protocol value here
|
|
89
|
+
// while this library continues to depend on 1.7.0.
|
|
90
|
+
private const val ROW_IMAGE_TYPE_MEDIUM = 16
|
|
91
|
+
|
|
87
92
|
const val TAG = "Parser"
|
|
88
93
|
|
|
89
94
|
fun parseHeader(
|
|
@@ -397,7 +402,13 @@ object Parser {
|
|
|
397
402
|
addText(parseText(detailedText))
|
|
398
403
|
}
|
|
399
404
|
row.image?.let { image ->
|
|
400
|
-
|
|
405
|
+
val parsedImage = parseImage(context, image)
|
|
406
|
+
val imageType = row.imageType
|
|
407
|
+
if (imageType == null) {
|
|
408
|
+
setImage(parsedImage)
|
|
409
|
+
} else {
|
|
410
|
+
setImage(parsedImage, imageType.toRowImageType())
|
|
411
|
+
}
|
|
401
412
|
}
|
|
402
413
|
row.browsable?.let { browsable ->
|
|
403
414
|
setBrowsable(browsable)
|
|
@@ -426,6 +437,14 @@ object Parser {
|
|
|
426
437
|
}.build()
|
|
427
438
|
}
|
|
428
439
|
|
|
440
|
+
private fun ListImageType.toRowImageType(): Int = when (this) {
|
|
441
|
+
ListImageType.LARGE -> Row.IMAGE_TYPE_LARGE
|
|
442
|
+
ListImageType.MEDIUM -> ROW_IMAGE_TYPE_MEDIUM
|
|
443
|
+
ListImageType.SMALL -> Row.IMAGE_TYPE_SMALL
|
|
444
|
+
ListImageType.EXTRA_SMALL -> Row.IMAGE_TYPE_EXTRA_SMALL
|
|
445
|
+
ListImageType.ICON -> Row.IMAGE_TYPE_ICON
|
|
446
|
+
}
|
|
447
|
+
|
|
429
448
|
fun formatToTimestamp(time: DurationWithTimeZone): String {
|
|
430
449
|
val calendar = Calendar.getInstance().apply {
|
|
431
450
|
add(Calendar.SECOND, time.seconds.toInt())
|
|
@@ -836,4 +855,4 @@ object Parser {
|
|
|
836
855
|
}
|
|
837
856
|
}.build()
|
|
838
857
|
}
|
|
839
|
-
}
|
|
858
|
+
}
|
package/ios/Types.swift
CHANGED
|
@@ -10,9 +10,12 @@ import UIKit
|
|
|
10
10
|
extension UIImage {
|
|
11
11
|
static func makeToggleImage(
|
|
12
12
|
enabled: Bool,
|
|
13
|
-
maximumImageSize: CGSize
|
|
13
|
+
maximumImageSize: CGSize,
|
|
14
|
+
traitCollection: UITraitCollection
|
|
14
15
|
) -> UIImage {
|
|
15
|
-
let
|
|
16
|
+
let format = UIGraphicsImageRendererFormat()
|
|
17
|
+
format.scale = traitCollection.displayScale
|
|
18
|
+
let renderer = UIGraphicsImageRenderer(size: maximumImageSize, format: format)
|
|
16
19
|
return renderer.image { ctx in
|
|
17
20
|
// Draw the track as a pill, centered vertically
|
|
18
21
|
let trackHeight = maximumImageSize.height * 0.5
|
|
@@ -160,7 +160,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
160
160
|
templateId: templateId
|
|
161
161
|
)
|
|
162
162
|
|
|
163
|
-
let carPlayTemplate = template.getTemplate()
|
|
163
|
+
let carPlayTemplate = try template.getTemplate()
|
|
164
164
|
|
|
165
165
|
if carPlayTemplate is CPMapTemplate {
|
|
166
166
|
try await MainActor.run {
|
|
@@ -192,23 +192,35 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
192
192
|
|
|
193
193
|
await template.invalidate()
|
|
194
194
|
|
|
195
|
-
let
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
carPlayTemplate,
|
|
204
|
-
animated: animated
|
|
195
|
+
if #available(iOS 27.0, *), let panel = template.getPanel() as? CPMapPanel {
|
|
196
|
+
/// since only the top most panel gets a panelDidHide call when pressing the close button
|
|
197
|
+
/// we hide it globaly to avoid orphaned panels on the navigation stack
|
|
198
|
+
panel.showsCloseButton = false
|
|
199
|
+
|
|
200
|
+
try await interfaceController.pushPanel(
|
|
201
|
+
panel,
|
|
202
|
+
templateId: templateId
|
|
205
203
|
)
|
|
206
204
|
}
|
|
207
205
|
else {
|
|
208
|
-
let
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
206
|
+
let carPlayTemplate = try template.getTemplate()
|
|
207
|
+
|
|
208
|
+
if carPlayTemplate is CPAlertTemplate {
|
|
209
|
+
let animated = try await !interfaceController.dismissTemplate(
|
|
210
|
+
animated: false
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
let _ = try await interfaceController.presentTemplate(
|
|
214
|
+
carPlayTemplate,
|
|
215
|
+
animated: animated
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
let _ = try await interfaceController.pushTemplate(
|
|
220
|
+
carPlayTemplate,
|
|
221
|
+
animated: true
|
|
222
|
+
)
|
|
223
|
+
}
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
if let autoDismissMs = template.autoDismissMs {
|
|
@@ -217,7 +229,7 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
217
229
|
nanoseconds: UInt64(autoDismissMs) * 1_000_000
|
|
218
230
|
)
|
|
219
231
|
|
|
220
|
-
if interfaceController.
|
|
232
|
+
if interfaceController.isTopEntry(templateId: templateId)
|
|
221
233
|
|| interfaceController.interfaceController
|
|
222
234
|
.presentedTemplate?.id == templateId
|
|
223
235
|
{
|
|
@@ -147,11 +147,15 @@ class HybridMapTemplate: HybridMapTemplateSpec {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
-
func startNavigation(templateId: String, trip: TripConfig) throws {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
150
|
+
func startNavigation(templateId: String, trip: TripConfig) throws -> Promise<Void> {
|
|
151
|
+
return Promise.async {
|
|
152
|
+
try await MainActor.run {
|
|
153
|
+
try RootModule.withAutoPlayTemplate(templateId: templateId) {
|
|
154
|
+
(template: MapTemplate) in
|
|
155
|
+
let trip = Parser.parseTrip(tripConfig: trip)
|
|
156
|
+
template.startNavigation(trip: trip)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -168,4 +172,17 @@ class HybridMapTemplate: HybridMapTemplateSpec {
|
|
|
168
172
|
template.setManeuverState(state: state)
|
|
169
173
|
}
|
|
170
174
|
}
|
|
175
|
+
|
|
176
|
+
func updateOptionsPanel(templateId: String, config: NitroOptionsPanelConfig?) throws
|
|
177
|
+
-> Promise<Void>
|
|
178
|
+
{
|
|
179
|
+
return Promise.async {
|
|
180
|
+
try await MainActor.run {
|
|
181
|
+
try RootModule.withAutoPlayTemplate(templateId: templateId) {
|
|
182
|
+
(template: MapTemplate) in
|
|
183
|
+
template.updateOptionsPanel(config: config)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
171
188
|
}
|
|
@@ -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
|
}
|