@rdlabo/ionic-theme-ios27 1.2.1 → 2.0.0-beta.pr303.shaf4f5ca16816f

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 (195) hide show
  1. package/README.md +24 -280
  2. package/dist/css/components/ion-toolbar.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/index.d.ts +3 -2
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +3 -2
  8. package/dist/index.js.map +1 -1
  9. package/dist/layout/index.d.ts +18 -0
  10. package/dist/layout/index.d.ts.map +1 -0
  11. package/dist/layout/index.js +26 -0
  12. package/dist/layout/index.js.map +1 -0
  13. package/dist/segment/index.js +3 -3
  14. package/dist/segment/index.js.map +1 -1
  15. package/dist/sheets-of-glass/index.js +7 -7
  16. package/dist/sheets-of-glass/index.js.map +1 -1
  17. package/dist/tab-bar-searchable/dismiss-button.js +53 -3
  18. package/dist/tab-bar-searchable/dismiss-button.js.map +1 -1
  19. package/dist/tab-bar-searchable/index.js +18 -3
  20. package/dist/tab-bar-searchable/index.js.map +1 -1
  21. package/dist/transition/ios.transition.d.ts.map +1 -1
  22. package/dist/transition/ios.transition.js +7 -4
  23. package/dist/transition/ios.transition.js.map +1 -1
  24. package/dist/transition/native-ui.d.ts +6 -0
  25. package/dist/transition/native-ui.d.ts.map +1 -0
  26. package/dist/{native-integration/index.js → transition/native-ui.js} +16 -30
  27. package/dist/transition/native-ui.js.map +1 -0
  28. package/docs/adaptive-themes.md +151 -0
  29. package/docs/add-native-ui.md +53 -0
  30. package/docs/animation.md +1 -1
  31. package/docs/eslint.md +1 -1
  32. package/docs/features.md +3 -3
  33. package/docs/iphone-duo-with-original-theme.md +40 -28
  34. package/docs/iphone-duo.md +29 -32
  35. package/docs/migration.md +12 -24
  36. package/docs/setup.md +82 -0
  37. package/docs/special-markup.md +5 -5
  38. package/docs/using-ion-item-group.md +2 -2
  39. package/docs/vertical-bars-layout.md +47 -0
  40. package/package.json +12 -42
  41. package/src/index.ts +3 -1
  42. package/src/layout/index.ts +29 -0
  43. package/src/segment/index.ts +3 -3
  44. package/src/sheets-of-glass/index.ts +7 -7
  45. package/src/styles/components/ion-toolbar.scss +1 -1
  46. package/src/styles/vertical-bars.scss +15 -18
  47. package/src/tab-bar-searchable/dismiss-button.ts +73 -4
  48. package/src/tab-bar-searchable/index.ts +20 -3
  49. package/src/transition/ios.transition.ts +7 -4
  50. package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
  51. package/Package.swift +0 -21
  52. package/dist/_virtual/_rolldown/runtime.js +0 -13
  53. package/dist/native/components/index.d.ts +0 -19
  54. package/dist/native/components/index.d.ts.map +0 -1
  55. package/dist/native/components/index.js +0 -40
  56. package/dist/native/components/index.js.map +0 -1
  57. package/dist/native/components/ion-back-button.d.ts +0 -9
  58. package/dist/native/components/ion-back-button.d.ts.map +0 -1
  59. package/dist/native/components/ion-back-button.js +0 -20
  60. package/dist/native/components/ion-back-button.js.map +0 -1
  61. package/dist/native/components/ion-button.d.ts +0 -10
  62. package/dist/native/components/ion-button.d.ts.map +0 -1
  63. package/dist/native/components/ion-button.js +0 -23
  64. package/dist/native/components/ion-button.js.map +0 -1
  65. package/dist/native/components/ion-buttons.d.ts +0 -11
  66. package/dist/native/components/ion-buttons.d.ts.map +0 -1
  67. package/dist/native/components/ion-buttons.js +0 -30
  68. package/dist/native/components/ion-buttons.js.map +0 -1
  69. package/dist/native/components/ion-fab.d.ts +0 -10
  70. package/dist/native/components/ion-fab.d.ts.map +0 -1
  71. package/dist/native/components/ion-fab.js +0 -132
  72. package/dist/native/components/ion-fab.js.map +0 -1
  73. package/dist/native/components/ion-menu-button.d.ts +0 -13
  74. package/dist/native/components/ion-menu-button.d.ts.map +0 -1
  75. package/dist/native/components/ion-menu-button.js +0 -31
  76. package/dist/native/components/ion-menu-button.js.map +0 -1
  77. package/dist/native/components/ion-segment.d.ts +0 -11
  78. package/dist/native/components/ion-segment.d.ts.map +0 -1
  79. package/dist/native/components/ion-segment.js +0 -30
  80. package/dist/native/components/ion-segment.js.map +0 -1
  81. package/dist/native/components/ion-tab-bar.d.ts +0 -11
  82. package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
  83. package/dist/native/components/ion-tab-bar.js +0 -50
  84. package/dist/native/components/ion-tab-bar.js.map +0 -1
  85. package/dist/native/components/searchable-tabs.d.ts +0 -20
  86. package/dist/native/components/searchable-tabs.d.ts.map +0 -1
  87. package/dist/native/components/searchable-tabs.js +0 -383
  88. package/dist/native/components/searchable-tabs.js.map +0 -1
  89. package/dist/native/definitions.d.ts +0 -152
  90. package/dist/native/definitions.d.ts.map +0 -1
  91. package/dist/native/definitions.js +0 -0
  92. package/dist/native/foldable/fold-state.d.ts +0 -5
  93. package/dist/native/foldable/fold-state.d.ts.map +0 -1
  94. package/dist/native/foldable/fold-state.js +0 -14
  95. package/dist/native/foldable/fold-state.js.map +0 -1
  96. package/dist/native/foldable/types.d.ts +0 -12
  97. package/dist/native/foldable/types.d.ts.map +0 -1
  98. package/dist/native/foldable/types.js +0 -0
  99. package/dist/native/index.d.ts +0 -11
  100. package/dist/native/index.d.ts.map +0 -1
  101. package/dist/native/index.js +0 -242
  102. package/dist/native/index.js.map +0 -1
  103. package/dist/native/prehide.d.ts +0 -7
  104. package/dist/native/prehide.d.ts.map +0 -1
  105. package/dist/native/prehide.js +0 -230
  106. package/dist/native/prehide.js.map +0 -1
  107. package/dist/native/runtime.d.ts +0 -5
  108. package/dist/native/runtime.d.ts.map +0 -1
  109. package/dist/native/runtime.js +0 -640
  110. package/dist/native/runtime.js.map +0 -1
  111. package/dist/native/shared/candidate.d.ts +0 -21
  112. package/dist/native/shared/candidate.d.ts.map +0 -1
  113. package/dist/native/shared/candidate.js +0 -98
  114. package/dist/native/shared/candidate.js.map +0 -1
  115. package/dist/native/shared/crossfade.d.ts +0 -10
  116. package/dist/native/shared/crossfade.d.ts.map +0 -1
  117. package/dist/native/shared/crossfade.js +0 -50
  118. package/dist/native/shared/crossfade.js.map +0 -1
  119. package/dist/native/shared/dom.d.ts +0 -46
  120. package/dist/native/shared/dom.d.ts.map +0 -1
  121. package/dist/native/shared/dom.js +0 -172
  122. package/dist/native/shared/dom.js.map +0 -1
  123. package/dist/native/shared/events.d.ts +0 -11
  124. package/dist/native/shared/events.d.ts.map +0 -1
  125. package/dist/native/shared/events.js +0 -13
  126. package/dist/native/shared/events.js.map +0 -1
  127. package/dist/native/shared/icons.d.ts +0 -8
  128. package/dist/native/shared/icons.d.ts.map +0 -1
  129. package/dist/native/shared/icons.js +0 -61
  130. package/dist/native/shared/icons.js.map +0 -1
  131. package/dist/native/shared/modal.d.ts +0 -10
  132. package/dist/native/shared/modal.d.ts.map +0 -1
  133. package/dist/native/shared/modal.js +0 -160
  134. package/dist/native/shared/modal.js.map +0 -1
  135. package/dist/native/vertical-bars-web-search.d.ts +0 -9
  136. package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
  137. package/dist/native/vertical-bars-web-search.js +0 -84
  138. package/dist/native/vertical-bars-web-search.js.map +0 -1
  139. package/dist/native/vertical-bars-web.d.ts +0 -5
  140. package/dist/native/vertical-bars-web.d.ts.map +0 -1
  141. package/dist/native/vertical-bars-web.js +0 -341
  142. package/dist/native/vertical-bars-web.js.map +0 -1
  143. package/dist/native-integration/index.d.ts +0 -23
  144. package/dist/native-integration/index.d.ts.map +0 -1
  145. package/dist/native-integration/index.js.map +0 -1
  146. package/dist/native-integration/transition.d.ts +0 -5
  147. package/dist/native-integration/transition.d.ts.map +0 -1
  148. package/dist/native-integration/transition.js +0 -16
  149. package/dist/native-integration/transition.js.map +0 -1
  150. package/dist/vertical-bars.d.ts +0 -5
  151. package/dist/vertical-bars.js +0 -4
  152. package/docs/e2e-testing.md +0 -51
  153. package/docs/feedback.md +0 -65
  154. package/docs/native-ui-shell.md +0 -285
  155. package/docs/vertical-bars.md +0 -285
  156. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
  157. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
  158. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
  159. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -83
  160. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
  161. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
  162. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -221
  163. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -437
  164. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
  165. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -396
  166. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
  167. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
  168. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
  169. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
  170. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -650
  171. package/src/native/components/index.ts +0 -53
  172. package/src/native/components/ion-back-button.ts +0 -19
  173. package/src/native/components/ion-button.ts +0 -24
  174. package/src/native/components/ion-buttons.ts +0 -38
  175. package/src/native/components/ion-fab.ts +0 -151
  176. package/src/native/components/ion-menu-button.ts +0 -24
  177. package/src/native/components/ion-segment.ts +0 -27
  178. package/src/native/components/ion-tab-bar.ts +0 -50
  179. package/src/native/components/searchable-tabs.ts +0 -459
  180. package/src/native/definitions.ts +0 -191
  181. package/src/native/foldable/fold-state.ts +0 -9
  182. package/src/native/foldable/types.ts +0 -4
  183. package/src/native/index.ts +0 -281
  184. package/src/native/prehide.ts +0 -265
  185. package/src/native/runtime.ts +0 -781
  186. package/src/native/shared/candidate.ts +0 -150
  187. package/src/native/shared/crossfade.ts +0 -50
  188. package/src/native/shared/dom.ts +0 -286
  189. package/src/native/shared/events.ts +0 -8
  190. package/src/native/shared/icons.ts +0 -57
  191. package/src/native/shared/modal.ts +0 -157
  192. package/src/native/vertical-bars-web-search.ts +0 -80
  193. package/src/native/vertical-bars-web.ts +0 -411
  194. package/src/native-integration/transition.ts +0 -15
  195. package/src/vertical-bars.ts +0 -11
@@ -1,91 +0,0 @@
1
- import UIKit
2
-
3
- private final class ShellSegmentElement: UIAccessibilityElement {
4
- var activate: (() -> Bool)?
5
- override func accessibilityActivate() -> Bool { activate?() ?? false }
6
- }
7
-
8
- final class ShellSegment: UISegmentedControl {
9
- static let kind = ShellComponent.segment
10
- var labels: [String] = []
11
- private var items: [ShellItem] = []
12
- override func layoutSubviews() {
13
- super.layoutSubviews()
14
- let total = (0..<numberOfSegments).reduce(CGFloat.zero) { $0 + widthForSegment(at: $1) }
15
- var x: CGFloat = 0
16
- accessibilityElements = labels.prefix(numberOfSegments).enumerated().map { index, label in
17
- let width = total > 0 ? bounds.width * widthForSegment(at: index) / total : bounds.width / CGFloat(numberOfSegments)
18
- let element = ShellSegmentElement(accessibilityContainer: self)
19
- element.accessibilityLabel = label
20
- element.accessibilityTraits = [.button]
21
- if index == selectedSegmentIndex { element.accessibilityTraits.insert(.selected) }
22
- if !isEnabledForSegment(at: index) { element.accessibilityTraits.insert(.notEnabled) }
23
- let origin = effectiveUserInterfaceLayoutDirection == .rightToLeft ? bounds.width - x - width : x
24
- element.accessibilityFrameInContainerSpace = CGRect(x: origin, y: 0, width: width, height: bounds.height)
25
- element.activate = { [weak self] in
26
- guard let self, self.isEnabledForSegment(at: index) else { return false }
27
- self.selectedSegmentIndex = index
28
- self.sendActions(for: .valueChanged)
29
- self.setNeedsLayout()
30
- return true
31
- }
32
- x += width
33
- return element
34
- }
35
- }
36
-
37
- @available(iOS 26.0, *)
38
- static func make(_ node: ShellControl, scale: CGFloat, rendering: ShellRendering, activate: @escaping (String) -> Void) -> UISegmentedControl? {
39
- guard !node.items.isEmpty else { return nil }
40
- let control = ShellSegment(items: [])
41
- control.isAccessibilityElement = false
42
- control.apportionsSegmentWidthsByContent = false
43
- control.update(node, scale: scale, rendering: rendering)
44
- control.addAction(UIAction { [weak control] _ in
45
- guard let control, control.items.indices.contains(control.selectedSegmentIndex) else { return }
46
- activate(control.items[control.selectedSegmentIndex].id)
47
- }, for: .valueChanged)
48
- return control
49
- }
50
-
51
- @available(iOS 26.0, *)
52
- func update(_ node: ShellControl, scale: CGFloat, rendering: ShellRendering) {
53
- let previous = items
54
- let rebuilt = previous.map(\.id) != node.items.map(\.id)
55
- if rebuilt {
56
- removeAllSegments()
57
- for index in node.items.indices { insertSegment(withTitle: "", at: index, animated: false) }
58
- }
59
- if let first = node.items.first, previous.first?.content.fontSize != first.content.fontSize {
60
- setTitleTextAttributes([.font: UIFont.systemFont(ofSize: first.content.fontSize, weight: .medium)], for: .normal)
61
- }
62
- let direction: UISemanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
63
- if semanticContentAttribute != direction { semanticContentAttribute = direction }
64
- // Ionic button frames exclude their margins. Distribute the full track
65
- // width so UIKit's fixed segment widths do not shrink the projected track.
66
- let totalItemWidth = node.items.reduce(0.0) { $0 + $1.frame.width }
67
- let widthScale = totalItemWidth > 0 ? node.frame.width / totalItemWidth * scale : scale
68
- for (index, item) in node.items.enumerated() {
69
- let old = rebuilt ? nil : previous[index].content
70
- let content = item.content
71
- if old?.label != content.label || old?.icon != content.icon ||
72
- old?.iconWidth != content.iconWidth || old?.iconHeight != content.iconHeight ||
73
- old?.iconTemplate != content.iconTemplate || old?.color != content.color {
74
- let image = rendering.image(content)
75
- setImage(image, forSegmentAt: index)
76
- setTitle(image == nil ? content.label : nil, forSegmentAt: index)
77
- }
78
- if isEnabledForSegment(at: index) == content.disabled { setEnabled(!content.disabled, forSegmentAt: index) }
79
- let width = item.frame.width * widthScale
80
- if widthForSegment(at: index) != width { setWidth(width, forSegmentAt: index) }
81
- }
82
- items = node.items
83
- labels = items.map { $0.content.accessibilityLabel }
84
- accessibilityIdentifier = node.id
85
- let selected = items.firstIndex { $0.content.selected } ?? UISegmentedControl.noSegment
86
- // A native tap has already selected this index. Reassigning it can interrupt
87
- // UIKit's in-flight lens animation when the Web selection echoes back.
88
- if selectedSegmentIndex != selected { selectedSegmentIndex = selected }
89
- setNeedsLayout()
90
- }
91
- }
@@ -1,221 +0,0 @@
1
- import UIKit
2
-
3
- enum ShellTabBar {
4
- struct Anchor: Decodable, Equatable {
5
- let x: Double
6
- let y: Double
7
- var isValid: Bool { [0, 0.5, 1].contains(x) && [0, 1].contains(y) }
8
- }
9
-
10
- /// Optimistic tab selection until the Web selected state catches up (or times out).
11
- struct PendingSelection: Equatable {
12
- var id: String
13
- var until: CFAbsoluteTime
14
- static func start(_ id: String, ttl: CFTimeInterval = 1) -> PendingSelection {
15
- PendingSelection(id: id, until: CFAbsoluteTimeGetCurrent() + ttl)
16
- }
17
- }
18
-
19
- static let kind = ShellComponent.tabBar
20
- /// Tab bars waiting for an iOS 26 title-layout warmup after the next `fit`.
21
- private static var pendingTitleWarmup = Set<ObjectIdentifier>()
22
-
23
- static func make(_ node: ShellControl, rendering: ShellRendering, delegate: UITabBarDelegate) -> UITabBar {
24
- let control = UITabBar()
25
- configureLayout(control)
26
- #if DEBUG
27
- NSLog("[Native UI Shell] Created UITabBar %@", node.id)
28
- #endif
29
- control.delegate = delegate
30
- update(control, node: node, rendering: rendering)
31
- return control
32
- }
33
-
34
- static func configureLayout(_ tabBar: UITabBar) {
35
- // The reader accepts icon-top tabs. Keep that layout local to the native
36
- // bar instead of letting the iPad idiom turn it into inline items.
37
- if #available(iOS 17.0, *) {
38
- tabBar.traitOverrides.horizontalSizeClass = .compact
39
- tabBar.traitOverrides.verticalSizeClass = .regular
40
- }
41
- tabBar.itemPositioning = .fill
42
- }
43
-
44
- static func update(_ tabBar: UITabBar, node: ShellControl, rendering: ShellRendering) {
45
- var pending: PendingSelection?
46
- update(tabBar, node: node, rendering: rendering, pendingSelection: &pending)
47
- }
48
-
49
- static func update(_ tabBar: UITabBar, node: ShellControl, rendering: ShellRendering,
50
- pendingSelection: inout PendingSelection?) {
51
- let items = node.items
52
- let ids = items.map(\.id)
53
- var needsTitleWarmup = false
54
- // Keep UIKit's item identities and interaction state across DOM selection updates.
55
- if tabBar.items?.map({ $0.accessibilityIdentifier ?? "" }) != ids {
56
- tabBar.items = items.map { item in
57
- let tab = UITabBarItem(title: item.content.label, image: rendering.image(item.content), tag: 0)
58
- tab.accessibilityIdentifier = item.id
59
- return tab
60
- }
61
- needsTitleWarmup = true
62
- }
63
- let nativeItems = tabBar.items ?? []
64
- for (tab, item) in zip(nativeItems, items) {
65
- let title = item.content.label
66
- if tab.title != title {
67
- tab.title = title
68
- needsTitleWarmup = true
69
- }
70
- let icon = rendering.image(item.content)
71
- if tab.image !== icon {
72
- tab.image = icon
73
- needsTitleWarmup = true
74
- }
75
- if tab.selectedImage !== icon { tab.selectedImage = icon }
76
- tab.isEnabled = !item.content.disabled
77
- tab.accessibilityLabel = item.content.accessibilityLabel
78
- if applyTypography(item.content, to: tab) { needsTitleWarmup = true }
79
- applyBadge(item.content.badge, to: tab, rendering: rendering)
80
- }
81
- let domSelected = zip(nativeItems, items).first { $0.1.content.selected }
82
- let selectedItem = domSelected?.0
83
- if let pending = pendingSelection {
84
- let pendingItem = zip(nativeItems, items).first { $0.1.id == pending.id }
85
- let expired = CFAbsoluteTimeGetCurrent() >= pending.until
86
- let unavailable = pendingItem == nil || pendingItem!.1.content.disabled
87
- if unavailable || expired {
88
- // Ionic rejected the tap, the item vanished, or the echo timed out.
89
- pendingSelection = nil
90
- if tabBar.selectedItem !== selectedItem { tabBar.selectedItem = selectedItem }
91
- } else if domSelected?.1.id == pending.id {
92
- pendingSelection = nil
93
- if tabBar.selectedItem !== selectedItem { tabBar.selectedItem = selectedItem }
94
- } else if tabBar.selectedItem !== pendingItem!.0 {
95
- // Keep the optimistic native selection until the Web selected state catches up.
96
- tabBar.selectedItem = pendingItem!.0
97
- }
98
- } else if tabBar.selectedItem !== selectedItem {
99
- tabBar.selectedItem = selectedItem
100
- }
101
- tabBar.semanticContentAttribute = node.rtl ? .forceRightToLeft : .forceLeftToRight
102
- tabBar.accessibilityIdentifier = node.id
103
- // Warm after `fit` settles geometry — not on every motion-driven frame sync.
104
- if needsTitleWarmup { pendingTitleWarmup.insert(ObjectIdentifier(tabBar)) }
105
- }
106
-
107
- @discardableResult
108
- static func applyTypography(_ content: ShellItemContent, to item: UITabBarItem) -> Bool {
109
- let weight: UIFont.Weight
110
- switch content.fontWeight {
111
- case ..<150: weight = .ultraLight
112
- case ..<250: weight = .thin
113
- case ..<350: weight = .light
114
- case ..<450: weight = .regular
115
- case ..<550: weight = .medium
116
- case ..<650: weight = .semibold
117
- case ..<750: weight = .bold
118
- case ..<850: weight = .heavy
119
- default: weight = .black
120
- }
121
- let font = UIFont.systemFont(ofSize: content.fontSize, weight: weight)
122
- var changed = false
123
- for state in [UIControl.State.normal, .selected] {
124
- if item.titleTextAttributes(for: state)?[.font] as? UIFont != font {
125
- item.setTitleTextAttributes([.font: font], for: state)
126
- changed = true
127
- }
128
- }
129
- return changed
130
- }
131
-
132
- static func applyBadge(_ badge: ShellBadge?, to item: UITabBarItem, rendering: ShellRendering) {
133
- // nil removes a badge; an empty string keeps the native notification dot.
134
- item.badgeValue = badge?.value
135
- item.accessibilityValue = badge?.value
136
- item.badgeColor = badge.map { rendering.color($0.color) }
137
- let attributes = badge.map { [NSAttributedString.Key.foregroundColor: rendering.color($0.textColor)] }
138
- item.setBadgeTextAttributes(attributes, for: .normal)
139
- item.setBadgeTextAttributes(attributes, for: .selected)
140
- }
141
-
142
- static func fit(_ tabBar: UITabBar, node: ShellControl, bounds: CGRect) -> Bool {
143
- // Place a new bar before asking UIKit for its safe-area-dependent size.
144
- // Measuring at the initial zero frame makes a restored iPad bar 5pt shorter.
145
- if tabBar.bounds.isEmpty {
146
- tabBar.frame = bounds
147
- tabBar.layoutIfNeeded()
148
- }
149
- let fitted = tabBar.sizeThatFits(bounds.size)
150
- var size = CGSize(width: bounds.width,
151
- height: max(bounds.height, fitted.height))
152
- if tabBar.bounds.size != size { tabBar.bounds.size = size }
153
- tabBar.layoutIfNeeded()
154
- // UIKit's tab content occupies only part of UITabBar's outer frame.
155
- // Align that content to ion-tab-bar, whose CSS already owns placement.
156
- guard var content = contentFrame(tabBar) else {
157
- return false
158
- }
159
- // Reserve ion-tab-bar's width for the tab content, excluding UIKit's outer gutters.
160
- // UIKit can cap the content width for a small number of items.
161
- // Do not keep expanding the outer frame beyond its natural size.
162
- size.width = min(bounds.width + tabBar.bounds.width - content.width,
163
- max(bounds.width, fitted.width))
164
- if abs(tabBar.bounds.width - size.width) > 0.1 {
165
- tabBar.bounds.size = size
166
- tabBar.layoutIfNeeded()
167
- guard let measured = contentFrame(tabBar) else {
168
- return false
169
- }
170
- content = measured
171
- }
172
- // UIKit owns the adaptive platter size, including changes after badge/title updates.
173
- // Preserve the DOM placement anchor instead of requiring identical item widths.
174
- let anchor = node.tabBarAnchor
175
- let x = anchor?.x ?? 0
176
- let y = anchor?.y ?? 0
177
- let center = CGPoint(x: bounds.minX + (bounds.width - content.width) * x - content.minX + size.width / 2,
178
- y: bounds.minY + (bounds.height - content.height) * y - content.minY + size.height / 2)
179
- if tabBar.center != center { tabBar.center = center }
180
- if pendingTitleWarmup.remove(ObjectIdentifier(tabBar)) != nil {
181
- warmUnselectedTitleLayout(tabBar)
182
- }
183
- return true
184
- }
185
-
186
- /// iOS 26 only: programmatically visit each unselected item so UIKit measures
187
- /// its title the same way a user tap would. Selection does not call the
188
- /// tab-bar delegate. No-op on iOS 27+, where first paint already matches Web.
189
- static func warmUnselectedTitleLayout(_ tabBar: UITabBar) {
190
- let version = ProcessInfo.processInfo.operatingSystemVersion
191
- guard version.majorVersion == 26, let items = tabBar.items, items.count > 1 else { return }
192
- let selected = tabBar.selectedItem
193
- UIView.performWithoutAnimation {
194
- for item in items where item !== selected {
195
- tabBar.selectedItem = item
196
- tabBar.layoutIfNeeded()
197
- }
198
- if tabBar.selectedItem !== selected {
199
- tabBar.selectedItem = selected
200
- tabBar.layoutIfNeeded()
201
- }
202
- }
203
- }
204
-
205
- private static func contentFrame(_ tabBar: UITabBar) -> CGRect? {
206
- contentFrames(tabBar).reduce(nil) { (result: CGRect?, frame) in result.map { $0.union(frame) } ?? frame }
207
- }
208
-
209
- static func contentFrames(_ tabBar: UITabBar) -> [CGRect] {
210
- contentViews(tabBar).map { $0.frame }
211
- }
212
-
213
- static func contentViews(_ tabBar: UITabBar) -> [UIView] {
214
- func containsControl(_ view: UIView) -> Bool {
215
- view is UIControl || view.subviews.contains(where: containsControl)
216
- }
217
- return tabBar.subviews
218
- .filter { !$0.isHidden && $0.alpha > 0 && containsControl($0) }
219
- .filter { !$0.frame.isEmpty }
220
- }
221
- }