@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.
Files changed (122) hide show
  1. package/README.md +214 -29
  2. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/AndroidAutoSession.kt +5 -0
  3. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplate.kt +9 -2
  4. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/NativeBackdrop.kt +40 -0
  5. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/VirtualRenderer.kt +52 -0
  6. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/GridTemplate.kt +47 -13
  7. package/android/src/main/java/com/margelo/nitro/swe/iternio/reactnativeautoplay/template/Parser.kt +21 -2
  8. package/ios/Types.swift +3 -0
  9. package/ios/extensions/UIImage+Toggle.swift +5 -2
  10. package/ios/hybrid/HybridAutoPlay.swift +28 -16
  11. package/ios/hybrid/HybridMapTemplate.swift +22 -5
  12. package/ios/scenes/AutoPlayInterfaceController.swift +261 -72
  13. package/ios/templates/AutoPlayMapPanelDelegate.swift +190 -0
  14. package/ios/templates/AutoPlayTemplate.swift +24 -2
  15. package/ios/templates/GridTemplate.swift +57 -57
  16. package/ios/templates/InformationTemplate.swift +84 -9
  17. package/ios/templates/ListTemplate.swift +66 -11
  18. package/ios/templates/MapTemplate.swift +83 -68
  19. package/ios/templates/MessageTemplate.swift +72 -6
  20. package/ios/templates/Parser.swift +564 -69
  21. package/ios/templates/SearchTemplate.swift +1 -1
  22. package/ios/templates/TemplateStore.swift +5 -15
  23. package/ios/utils/SymbolFont.swift +13 -28
  24. package/lib/Constants.d.ts +4 -0
  25. package/lib/Constants.js +6 -0
  26. package/lib/index.d.ts +1 -0
  27. package/lib/index.js +1 -0
  28. package/lib/specs/MapTemplate.nitro.d.ts +3 -1
  29. package/lib/templates/GridTemplate.d.ts +23 -3
  30. package/lib/templates/GridTemplate.js +8 -0
  31. package/lib/templates/InformationTemplate.d.ts +38 -12
  32. package/lib/templates/ListTemplate.d.ts +16 -47
  33. package/lib/templates/MapTemplate.d.ts +26 -2
  34. package/lib/templates/MapTemplate.js +16 -2
  35. package/lib/templates/MessageTemplate.d.ts +52 -11
  36. package/lib/templates/MessageTemplate.js +1 -1
  37. package/lib/utils/NitroOptionsPanel.d.ts +103 -0
  38. package/lib/utils/NitroOptionsPanel.js +63 -0
  39. package/lib/utils/NitroSection.d.ts +89 -2
  40. package/lib/utils/NitroSection.js +49 -2
  41. package/nitrogen/generated/android/ReactNativeAutoPlay+autolinking.cmake +1 -0
  42. package/nitrogen/generated/android/c++/JChargingConnector.hpp +79 -0
  43. package/nitrogen/generated/android/c++/JGridImageSize.hpp +64 -0
  44. package/nitrogen/generated/android/c++/JGridTemplateConfig.hpp +7 -1
  45. package/nitrogen/generated/android/c++/JHybridGridTemplateSpec.cpp +4 -0
  46. package/nitrogen/generated/android/c++/JHybridInformationTemplateSpec.cpp +12 -0
  47. package/nitrogen/generated/android/c++/JHybridListTemplateSpec.cpp +12 -0
  48. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.cpp +78 -3
  49. package/nitrogen/generated/android/c++/JHybridMapTemplateSpec.hpp +2 -1
  50. package/nitrogen/generated/android/c++/JHybridSearchTemplateSpec.cpp +12 -0
  51. package/nitrogen/generated/android/c++/JInformationTemplateConfig.hpp +6 -0
  52. package/nitrogen/generated/android/c++/JListImageType.hpp +67 -0
  53. package/nitrogen/generated/android/c++/JListTemplateConfig.hpp +6 -0
  54. package/nitrogen/generated/android/c++/JNitroChargerLocation.hpp +115 -0
  55. package/nitrogen/generated/android/c++/JNitroChargerOutlet.hpp +82 -0
  56. package/nitrogen/generated/android/c++/JNitroOptionsPanelChargerSection.hpp +112 -0
  57. package/nitrogen/generated/android/c++/JNitroOptionsPanelConfig.hpp +157 -0
  58. package/nitrogen/generated/android/c++/JNitroOptionsPanelGridSection.hpp +102 -0
  59. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.cpp +30 -0
  60. package/nitrogen/generated/android/c++/JNitroOptionsPanelSection.hpp +129 -0
  61. package/nitrogen/generated/android/c++/JNitroRow.hpp +33 -3
  62. package/nitrogen/generated/android/c++/JNitroSection.hpp +6 -0
  63. package/nitrogen/generated/android/c++/JSearchTemplateConfig.hpp +6 -0
  64. package/nitrogen/generated/android/c++/JWaypointCoordinate.hpp +65 -0
  65. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ChargingConnector.kt +30 -0
  66. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridImageSize.kt +25 -0
  67. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/GridTemplateConfig.kt +9 -4
  68. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/HybridMapTemplateSpec.kt +5 -1
  69. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/ListImageType.kt +26 -0
  70. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerLocation.kt +90 -0
  71. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroChargerOutlet.kt +70 -0
  72. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelChargerSection.kt +61 -0
  73. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelConfig.kt +61 -0
  74. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelGridSection.kt +56 -0
  75. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroOptionsPanelSection.kt +75 -0
  76. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/NitroRow.kt +36 -6
  77. package/nitrogen/generated/android/kotlin/com/margelo/nitro/swe/iternio/reactnativeautoplay/WaypointCoordinate.kt +61 -0
  78. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Bridge.hpp +174 -0
  79. package/nitrogen/generated/ios/ReactNativeAutoPlay-Swift-Cxx-Umbrella.hpp +27 -0
  80. package/nitrogen/generated/ios/c++/HybridGridTemplateSpecSwift.hpp +3 -0
  81. package/nitrogen/generated/ios/c++/HybridInformationTemplateSpecSwift.hpp +9 -0
  82. package/nitrogen/generated/ios/c++/HybridListTemplateSpecSwift.hpp +9 -0
  83. package/nitrogen/generated/ios/c++/HybridMapTemplateSpecSwift.hpp +47 -1
  84. package/nitrogen/generated/ios/c++/HybridSearchTemplateSpecSwift.hpp +9 -0
  85. package/nitrogen/generated/ios/swift/ChargingConnector.swift +68 -0
  86. package/nitrogen/generated/ios/swift/GridImageSize.swift +48 -0
  87. package/nitrogen/generated/ios/swift/GridTemplateConfig.swift +12 -1
  88. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec.swift +2 -1
  89. package/nitrogen/generated/ios/swift/HybridMapTemplateSpec_cxx.swift +31 -4
  90. package/nitrogen/generated/ios/swift/ListImageType.swift +52 -0
  91. package/nitrogen/generated/ios/swift/NitroChargerLocation.swift +161 -0
  92. package/nitrogen/generated/ios/swift/NitroChargerOutlet.swift +65 -0
  93. package/nitrogen/generated/ios/swift/NitroOptionsPanelChargerSection.swift +64 -0
  94. package/nitrogen/generated/ios/swift/NitroOptionsPanelConfig.swift +94 -0
  95. package/nitrogen/generated/ios/swift/NitroOptionsPanelGridSection.swift +53 -0
  96. package/nitrogen/generated/ios/swift/NitroOptionsPanelSection.swift +32 -0
  97. package/nitrogen/generated/ios/swift/NitroRow.swift +81 -1
  98. package/nitrogen/generated/ios/swift/WaypointCoordinate.swift +52 -0
  99. package/nitrogen/generated/shared/c++/ChargingConnector.hpp +104 -0
  100. package/nitrogen/generated/shared/c++/GridImageSize.hpp +84 -0
  101. package/nitrogen/generated/shared/c++/GridTemplateConfig.hpp +8 -1
  102. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.cpp +1 -0
  103. package/nitrogen/generated/shared/c++/HybridMapTemplateSpec.hpp +5 -1
  104. package/nitrogen/generated/shared/c++/ListImageType.hpp +88 -0
  105. package/nitrogen/generated/shared/c++/NitroChargerLocation.hpp +131 -0
  106. package/nitrogen/generated/shared/c++/NitroChargerOutlet.hpp +98 -0
  107. package/nitrogen/generated/shared/c++/NitroOptionsPanelChargerSection.hpp +98 -0
  108. package/nitrogen/generated/shared/c++/NitroOptionsPanelConfig.hpp +107 -0
  109. package/nitrogen/generated/shared/c++/NitroOptionsPanelGridSection.hpp +91 -0
  110. package/nitrogen/generated/shared/c++/NitroRow.hpp +39 -2
  111. package/nitrogen/generated/shared/c++/WaypointCoordinate.hpp +91 -0
  112. package/package.json +1 -1
  113. package/src/Constants.ts +9 -0
  114. package/src/index.ts +1 -0
  115. package/src/specs/MapTemplate.nitro.ts +3 -1
  116. package/src/templates/GridTemplate.ts +38 -3
  117. package/src/templates/InformationTemplate.ts +60 -29
  118. package/src/templates/ListTemplate.ts +27 -55
  119. package/src/templates/MapTemplate.ts +60 -2
  120. package/src/templates/MessageTemplate.ts +71 -24
  121. package/src/utils/NitroOptionsPanel.ts +218 -0
  122. package/src/utils/NitroSection.ts +160 -5
@@ -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) {}
@@ -8,92 +8,92 @@
8
8
  import CarPlay
9
9
 
10
10
  class GridTemplate: AutoPlayHeaderProviding {
11
- let template: CPGridTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPGridTemplate?
12
13
  var config: GridTemplateConfig
13
14
 
14
15
  var buttons: [NitroGridButton]
15
16
 
17
+ /// Boxed as `Any` since `CPMapPanel` itself is only available on iOS 27+ while this class supports a much lower deployment target.
18
+ private var mapPanel: Any?
19
+
20
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
21
+ private var mapPanelDelegate: Any?
22
+
16
23
  override var autoDismissMs: Double? {
17
24
  return config.autoDismissMs
18
25
  }
19
26
 
20
- override func getTemplate() -> CPTemplate {
27
+ override func getTemplate() throws -> CPTemplate {
28
+ guard let template else {
29
+ throw AutoPlayError.templateIsMapPanel(config.id)
30
+ }
21
31
  return template
22
32
  }
23
33
 
34
+ override func getPanel() -> Any? {
35
+ return mapPanel
36
+ }
37
+
38
+ override func getPanelHeaderActions() -> [NitroAction]? {
39
+ return barButtons
40
+ }
41
+
42
+ override func getPanelMapButtons() -> [NitroMapButton]? {
43
+ return config.mapConfig?.mapButtons
44
+ }
45
+
24
46
  init(config: GridTemplateConfig) {
25
47
  self.config = config
26
48
  buttons = config.buttons
27
49
 
28
- template = CPGridTemplate(
29
- title: Parser.parseText(text: config.title),
30
- gridButtons: GridTemplate.parseButtons(buttons: buttons),
31
- id: config.id
32
- )
50
+ let title = Parser.parseText(text: config.title)
51
+
52
+ if #available(iOS 27.0, *), config.mapConfig != nil {
53
+ mapPanel = CPMapPanel(
54
+ title: title,
55
+ sections: [CPMapPanelSection(title: nil, items: [])],
56
+ buttonConfiguration: nil
57
+ )
58
+ }
59
+ else {
60
+ template = CPGridTemplate(
61
+ title: title,
62
+ gridButtons: GridTemplate.parseButtons(buttons: buttons),
63
+ id: config.id
64
+ )
65
+ }
33
66
 
34
67
  super.init()
35
68
 
36
69
  barButtons = config.headerActions
37
70
 
71
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
72
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
73
+ mapPanelDelegate = delegate
74
+ mapPanel.delegate = delegate
75
+ }
38
76
  }
39
77
 
40
78
  static func parseButtons(buttons: [NitroGridButton]) -> [CPGridButton] {
41
- let gridButtonHeight: CGFloat
42
-
43
- if #available(iOS 26.0, *) {
44
- gridButtonHeight = CPGridTemplate.maximumGridButtonImageSize.height
45
- }
46
- else {
47
- gridButtonHeight = 44
48
- }
49
-
50
- guard let traitCollection = SceneStore.getRootTraitCollection() else {
51
- return []
52
- }
53
-
54
- return buttons.compactMap { button in
55
- var image: UIImage?
56
-
57
- if let glyphImage = button.image.glyphImage {
58
- image = SymbolFont.imageFromNitroImage(
59
- image: glyphImage,
60
- size: gridButtonHeight,
61
- traitCollection: traitCollection
62
- )
63
- }
64
-
65
- if let assetImage = button.image.assetImage {
66
- image = Parser.parseAssetImage(
67
- assetImage: assetImage,
68
- traitCollection: traitCollection
69
- )
70
- }
71
-
72
- if let remoteImage = button.image.remoteImage {
73
- image = Parser.parseRemoteImage(
74
- remoteImage: remoteImage,
75
- traitCollection: traitCollection
76
- )
77
- }
78
-
79
- guard let image = image else { return nil }
80
- guard let title = Parser.parseText(text: button.title) else { return nil }
81
-
82
- return CPGridButton(
83
- titleVariants: [title],
84
- image: image
85
- ) { _ in
86
- button.onPress()
87
- }
88
- }
79
+ guard let traitCollection = SceneStore.getRootTraitCollection() else { return [] }
80
+ return Parser.parseGridButtons(buttons: buttons, traitCollection: traitCollection)
89
81
  }
90
82
 
91
83
  @MainActor
92
84
  override func _invalidate() {
85
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
86
+ let parsedButtons = GridTemplate.parseButtons(buttons: buttons)
87
+ mapPanel.sections.first?.items = [CPMapPanelItem(gridButtons: parsedButtons)]
88
+ reapplyPanelBarIfVisible(self, templateId: config.id)
89
+ return
90
+ }
91
+
92
+ guard let template else { return }
93
+
93
94
  setBarButtons(template: template, barButtons: barButtons)
94
95
 
95
- let buttons = GridTemplate.parseButtons(buttons: buttons)
96
- template.updateGridButtons(buttons)
96
+ template.updateGridButtons(GridTemplate.parseButtons(buttons: buttons))
97
97
  }
98
98
 
99
99
  override func onWillAppear(animated: Bool) {
@@ -8,31 +8,69 @@
8
8
  import CarPlay
9
9
 
10
10
  class InformationTemplate: AutoPlayHeaderProviding {
11
- let template: CPInformationTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPInformationTemplate?
12
13
  var config: InformationTemplateConfig
13
14
 
14
15
  var section: NitroSection
15
16
 
17
+ /// Whether this template renders as a `CPMapPanel` instead of a plain `CPInformationTemplate`.
18
+ private let usesMapPanel: Bool
19
+
20
+ /// Built lazily on the first `_invalidate()` call rather than in `init()`: `CPMapPanel.buttonConfiguration`
21
+ /// is readonly, so unlike sections it can't be patched in later, and building it needs
22
+ /// `traitCollection` for the optional symbol button image — only safe to fetch on the
23
+ /// main thread, which `init()` isn't guaranteed to run on. Boxed as `Any` since `CPMapPanel`
24
+ /// itself is only available on iOS 27+ while this class supports a much lower deployment target.
25
+ private var mapPanel: Any?
26
+
27
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
28
+ private var mapPanelDelegate: Any?
29
+
16
30
  override var autoDismissMs: Double? {
17
31
  return config.autoDismissMs
18
32
  }
19
33
 
20
- override func getTemplate() -> CPTemplate {
34
+ override func getTemplate() throws -> CPTemplate {
35
+ guard let template else {
36
+ throw AutoPlayError.templateIsMapPanel(config.id)
37
+ }
21
38
  return template
22
39
  }
23
40
 
41
+ override func getPanel() -> Any? {
42
+ return mapPanel
43
+ }
44
+
45
+ override func getPanelHeaderActions() -> [NitroAction]? {
46
+ return barButtons
47
+ }
48
+
49
+ override func getPanelMapButtons() -> [NitroMapButton]? {
50
+ return config.mapConfig?.mapButtons
51
+ }
52
+
24
53
  init(config: InformationTemplateConfig) {
25
54
  self.config = config
26
55
 
27
56
  section = config.section
28
57
 
29
- template = CPInformationTemplate(
30
- title: Parser.parseText(text: config.title)!,
31
- layout: .leading,
32
- items: Parser.parseInformationItems(section: section),
33
- actions: Parser.parseInformationActions(actions: config.actions),
34
- id: config.id
35
- )
58
+ if #available(iOS 27.0, *) {
59
+ usesMapPanel = config.mapConfig != nil
60
+ }
61
+ else {
62
+ usesMapPanel = false
63
+ }
64
+
65
+ if !usesMapPanel {
66
+ template = CPInformationTemplate(
67
+ title: Parser.parseText(text: config.title)!,
68
+ layout: .leading,
69
+ items: Parser.parseInformationItems(section: section),
70
+ actions: Parser.parseInformationActions(actions: config.actions),
71
+ id: config.id
72
+ )
73
+ }
36
74
 
37
75
  super.init()
38
76
 
@@ -41,6 +79,43 @@ class InformationTemplate: AutoPlayHeaderProviding {
41
79
 
42
80
  @MainActor
43
81
  override func _invalidate() {
82
+ if usesMapPanel, #available(iOS 27.0, *) {
83
+ guard let mapPanel = mapPanel as? CPMapPanel else {
84
+ guard let traitCollection = SceneStore.getRootTraitCollection() else { return }
85
+
86
+ // InformationTemplate's actions are only ever set once and never updated
87
+ // afterwards, so there's nothing to keep in sync with buttonConfiguration later.
88
+ let panel = CPMapPanel(
89
+ title: Parser.parseText(text: config.title),
90
+ sections: [
91
+ CPMapPanelSection(
92
+ title: nil,
93
+ items: Parser.parseInformationPanelItems(section: section)
94
+ )
95
+ ],
96
+ buttonConfiguration: Parser.parsePanelButtonConfiguration(
97
+ actions: config.actions,
98
+ traitCollection: traitCollection
99
+ )
100
+ )
101
+
102
+ mapPanel = panel
103
+
104
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
105
+ mapPanelDelegate = delegate
106
+ panel.delegate = delegate
107
+
108
+ reapplyPanelBarIfVisible(self, templateId: config.id)
109
+ return
110
+ }
111
+
112
+ mapPanel.sections.first?.items = Parser.parseInformationPanelItems(section: section)
113
+ reapplyPanelBarIfVisible(self, templateId: config.id)
114
+ return
115
+ }
116
+
117
+ guard let template else { return }
118
+
44
119
  setBarButtons(template: template, barButtons: barButtons)
45
120
  template.items = Parser.parseInformationItems(section: section)
46
121
  }
@@ -8,44 +8,99 @@
8
8
  import CarPlay
9
9
 
10
10
  class ListTemplate: AutoPlayHeaderProviding {
11
- let template: CPListTemplate
11
+ /// `nil` when this template renders as a map panel instead (see `mapPanel`).
12
+ private(set) var template: CPListTemplate?
12
13
  var config: ListTemplateConfig
13
14
 
14
15
  var sections: [NitroSection]?
15
16
 
17
+ /// Boxed as `Any` since `CPMapPanel` itself is only available on
18
+ /// iOS 27+ while this class supports a much lower deployment target.
19
+ private var mapPanel: Any?
20
+
21
+ /// `CPMapPanel.delegate` is `weak`, so this keeps the shared delegate alive for as long as
22
+ /// the panel exists. Boxed as `Any` for the same reason as `mapPanel`.
23
+ private var mapPanelDelegate: Any?
24
+
16
25
  override var autoDismissMs: Double? {
17
26
  return config.autoDismissMs
18
27
  }
19
28
 
20
- override func getTemplate() -> CPTemplate {
29
+ override func getTemplate() throws -> CPTemplate {
30
+ guard let template else {
31
+ throw AutoPlayError.templateIsMapPanel(config.id)
32
+ }
21
33
  return template
22
34
  }
23
35
 
36
+ override func getPanel() -> Any? {
37
+ return mapPanel
38
+ }
39
+
40
+ override func getPanelHeaderActions() -> [NitroAction]? {
41
+ return barButtons
42
+ }
43
+
44
+ override func getPanelMapButtons() -> [NitroMapButton]? {
45
+ return config.mapConfig?.mapButtons
46
+ }
47
+
24
48
  init(config: ListTemplateConfig) {
25
49
  self.config = config
26
50
 
27
51
  sections = config.sections
28
-
29
- template = CPListTemplate(
30
- title: Parser.parseText(text: config.title),
31
- sections: [],
32
- assistantCellConfiguration: nil,
33
- id: config.id
34
- )
52
+ let title = Parser.parseText(text: config.title)
53
+
54
+ if #available(iOS 27.0, *), config.mapConfig != nil {
55
+ mapPanel = CPMapPanel(
56
+ title: title,
57
+ sections: (config.sections ?? []).map { section in
58
+ CPMapPanelSection(title: section.title, items: [])
59
+ },
60
+ buttonConfiguration: nil
61
+ )
62
+ }
63
+ else {
64
+ template = CPListTemplate(
65
+ title: title,
66
+ sections: [],
67
+ assistantCellConfiguration: nil,
68
+ id: config.id
69
+ )
70
+ }
35
71
 
36
72
  super.init()
37
73
 
38
74
  barButtons = config.headerActions
75
+
76
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
77
+ let delegate = AutoPlayMapPanelDelegate(template: self, templateId: config.id)
78
+ mapPanelDelegate = delegate
79
+ mapPanel.delegate = delegate
80
+ }
39
81
  }
40
82
 
41
83
  @MainActor
42
84
  override func _invalidate() {
43
- setBarButtons(template: template, barButtons: barButtons)
44
-
45
85
  guard let traitCollection = SceneStore.getRootTraitCollection() else {
46
86
  return
47
87
  }
48
88
 
89
+ if #available(iOS 27.0, *), let mapPanel = mapPanel as? CPMapPanel {
90
+ mapPanel.sections = Parser.parseMapPanelSections(
91
+ sections: sections,
92
+ updateSection: self.updateSection(section:sectionIndex:),
93
+ traitCollection: traitCollection
94
+ )
95
+
96
+ reapplyPanelBarIfVisible(self, templateId: config.id)
97
+ return
98
+ }
99
+
100
+ guard let template else { return }
101
+
102
+ setBarButtons(template: template, barButtons: barButtons)
103
+
49
104
  template.updateSections(
50
105
  Parser.parseSections(
51
106
  sections: sections,