@rdlabo/ionic-theme-ios27 1.2.1 → 2.0.0-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 +24 -280
- package/dist/css/components/ion-toolbar.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/layout/index.d.ts +18 -0
- package/dist/layout/index.d.ts.map +1 -0
- package/dist/layout/index.js +26 -0
- package/dist/layout/index.js.map +1 -0
- package/dist/segment/index.js +3 -3
- package/dist/segment/index.js.map +1 -1
- package/dist/sheets-of-glass/index.js +7 -7
- package/dist/sheets-of-glass/index.js.map +1 -1
- package/dist/tab-bar-searchable/dismiss-button.js +53 -3
- package/dist/tab-bar-searchable/dismiss-button.js.map +1 -1
- package/dist/tab-bar-searchable/index.js +18 -3
- package/dist/tab-bar-searchable/index.js.map +1 -1
- package/dist/transition/ios.transition.d.ts.map +1 -1
- package/dist/transition/ios.transition.js +7 -4
- package/dist/transition/ios.transition.js.map +1 -1
- package/dist/transition/native-ui.d.ts +6 -0
- package/dist/transition/native-ui.d.ts.map +1 -0
- package/dist/{native-integration/index.js → transition/native-ui.js} +16 -30
- package/dist/transition/native-ui.js.map +1 -0
- package/docs/adaptive-themes.md +161 -0
- package/docs/add-native-ui.md +53 -0
- package/docs/animation.md +19 -3
- package/docs/eslint.md +3 -1
- package/docs/features.md +9 -5
- package/docs/iphone-duo.md +62 -35
- package/docs/migration.md +17 -26
- package/docs/setup.md +92 -0
- package/docs/special-markup.md +41 -10
- package/docs/using-ion-item-group.md +2 -2
- package/docs/vertical-bars-layout.md +67 -0
- package/package.json +12 -42
- package/src/index.ts +3 -1
- package/src/layout/index.ts +29 -0
- package/src/segment/index.ts +3 -3
- package/src/sheets-of-glass/index.ts +7 -7
- package/src/styles/components/ion-toolbar.scss +1 -1
- package/src/styles/vertical-bars.scss +15 -18
- package/src/tab-bar-searchable/dismiss-button.ts +73 -4
- package/src/tab-bar-searchable/index.ts +20 -3
- package/src/transition/ios.transition.ts +7 -4
- package/src/{native-integration/index.ts → transition/native-ui.ts} +17 -46
- package/Package.swift +0 -21
- package/dist/_virtual/_rolldown/runtime.js +0 -13
- package/dist/native/components/index.d.ts +0 -19
- package/dist/native/components/index.d.ts.map +0 -1
- package/dist/native/components/index.js +0 -40
- package/dist/native/components/index.js.map +0 -1
- package/dist/native/components/ion-back-button.d.ts +0 -9
- package/dist/native/components/ion-back-button.d.ts.map +0 -1
- package/dist/native/components/ion-back-button.js +0 -20
- package/dist/native/components/ion-back-button.js.map +0 -1
- package/dist/native/components/ion-button.d.ts +0 -10
- package/dist/native/components/ion-button.d.ts.map +0 -1
- package/dist/native/components/ion-button.js +0 -23
- package/dist/native/components/ion-button.js.map +0 -1
- package/dist/native/components/ion-buttons.d.ts +0 -11
- package/dist/native/components/ion-buttons.d.ts.map +0 -1
- package/dist/native/components/ion-buttons.js +0 -30
- package/dist/native/components/ion-buttons.js.map +0 -1
- package/dist/native/components/ion-fab.d.ts +0 -10
- package/dist/native/components/ion-fab.d.ts.map +0 -1
- package/dist/native/components/ion-fab.js +0 -132
- package/dist/native/components/ion-fab.js.map +0 -1
- package/dist/native/components/ion-menu-button.d.ts +0 -13
- package/dist/native/components/ion-menu-button.d.ts.map +0 -1
- package/dist/native/components/ion-menu-button.js +0 -31
- package/dist/native/components/ion-menu-button.js.map +0 -1
- package/dist/native/components/ion-segment.d.ts +0 -11
- package/dist/native/components/ion-segment.d.ts.map +0 -1
- package/dist/native/components/ion-segment.js +0 -30
- package/dist/native/components/ion-segment.js.map +0 -1
- package/dist/native/components/ion-tab-bar.d.ts +0 -11
- package/dist/native/components/ion-tab-bar.d.ts.map +0 -1
- package/dist/native/components/ion-tab-bar.js +0 -50
- package/dist/native/components/ion-tab-bar.js.map +0 -1
- package/dist/native/components/searchable-tabs.d.ts +0 -20
- package/dist/native/components/searchable-tabs.d.ts.map +0 -1
- package/dist/native/components/searchable-tabs.js +0 -383
- package/dist/native/components/searchable-tabs.js.map +0 -1
- package/dist/native/definitions.d.ts +0 -152
- package/dist/native/definitions.d.ts.map +0 -1
- package/dist/native/definitions.js +0 -0
- package/dist/native/foldable/fold-state.d.ts +0 -5
- package/dist/native/foldable/fold-state.d.ts.map +0 -1
- package/dist/native/foldable/fold-state.js +0 -14
- package/dist/native/foldable/fold-state.js.map +0 -1
- package/dist/native/foldable/types.d.ts +0 -12
- package/dist/native/foldable/types.d.ts.map +0 -1
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/index.d.ts +0 -11
- package/dist/native/index.d.ts.map +0 -1
- package/dist/native/index.js +0 -242
- package/dist/native/index.js.map +0 -1
- package/dist/native/prehide.d.ts +0 -7
- package/dist/native/prehide.d.ts.map +0 -1
- package/dist/native/prehide.js +0 -230
- package/dist/native/prehide.js.map +0 -1
- package/dist/native/runtime.d.ts +0 -5
- package/dist/native/runtime.d.ts.map +0 -1
- package/dist/native/runtime.js +0 -640
- package/dist/native/runtime.js.map +0 -1
- package/dist/native/shared/candidate.d.ts +0 -21
- package/dist/native/shared/candidate.d.ts.map +0 -1
- package/dist/native/shared/candidate.js +0 -98
- package/dist/native/shared/candidate.js.map +0 -1
- package/dist/native/shared/crossfade.d.ts +0 -10
- package/dist/native/shared/crossfade.d.ts.map +0 -1
- package/dist/native/shared/crossfade.js +0 -50
- package/dist/native/shared/crossfade.js.map +0 -1
- package/dist/native/shared/dom.d.ts +0 -46
- package/dist/native/shared/dom.d.ts.map +0 -1
- package/dist/native/shared/dom.js +0 -172
- package/dist/native/shared/dom.js.map +0 -1
- package/dist/native/shared/events.d.ts +0 -11
- package/dist/native/shared/events.d.ts.map +0 -1
- package/dist/native/shared/events.js +0 -13
- package/dist/native/shared/events.js.map +0 -1
- package/dist/native/shared/icons.d.ts +0 -8
- package/dist/native/shared/icons.d.ts.map +0 -1
- package/dist/native/shared/icons.js +0 -61
- package/dist/native/shared/icons.js.map +0 -1
- package/dist/native/shared/modal.d.ts +0 -10
- package/dist/native/shared/modal.d.ts.map +0 -1
- package/dist/native/shared/modal.js +0 -160
- package/dist/native/shared/modal.js.map +0 -1
- package/dist/native/vertical-bars-web-search.d.ts +0 -9
- package/dist/native/vertical-bars-web-search.d.ts.map +0 -1
- package/dist/native/vertical-bars-web-search.js +0 -84
- package/dist/native/vertical-bars-web-search.js.map +0 -1
- package/dist/native/vertical-bars-web.d.ts +0 -5
- package/dist/native/vertical-bars-web.d.ts.map +0 -1
- package/dist/native/vertical-bars-web.js +0 -341
- package/dist/native/vertical-bars-web.js.map +0 -1
- package/dist/native-integration/index.d.ts +0 -23
- package/dist/native-integration/index.d.ts.map +0 -1
- package/dist/native-integration/index.js.map +0 -1
- package/dist/native-integration/transition.d.ts +0 -5
- package/dist/native-integration/transition.d.ts.map +0 -1
- package/dist/native-integration/transition.js +0 -16
- package/dist/native-integration/transition.js.map +0 -1
- package/dist/vertical-bars.d.ts +0 -5
- package/dist/vertical-bars.js +0 -4
- package/docs/e2e-testing.md +0 -51
- package/docs/feedback.md +0 -65
- package/docs/iphone-duo-with-original-theme.md +0 -158
- package/docs/native-ui-shell.md +0 -285
- package/docs/vertical-bars.md +0 -285
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButton.swift +0 -71
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellButtons.swift +0 -24
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellComponents.swift +0 -18
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellFab.swift +0 -83
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +0 -432
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSegment.swift +0 -91
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +0 -221
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +0 -437
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +0 -118
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +0 -396
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellCrossfade.swift +0 -35
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellHost.swift +0 -8
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellRendering.swift +0 -27
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +0 -135
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +0 -650
- package/src/native/components/index.ts +0 -53
- package/src/native/components/ion-back-button.ts +0 -19
- package/src/native/components/ion-button.ts +0 -24
- package/src/native/components/ion-buttons.ts +0 -38
- package/src/native/components/ion-fab.ts +0 -151
- package/src/native/components/ion-menu-button.ts +0 -24
- package/src/native/components/ion-segment.ts +0 -27
- package/src/native/components/ion-tab-bar.ts +0 -50
- package/src/native/components/searchable-tabs.ts +0 -459
- package/src/native/definitions.ts +0 -191
- package/src/native/foldable/fold-state.ts +0 -9
- package/src/native/foldable/types.ts +0 -4
- package/src/native/index.ts +0 -281
- package/src/native/prehide.ts +0 -265
- package/src/native/runtime.ts +0 -781
- package/src/native/shared/candidate.ts +0 -150
- package/src/native/shared/crossfade.ts +0 -50
- package/src/native/shared/dom.ts +0 -286
- package/src/native/shared/events.ts +0 -8
- package/src/native/shared/icons.ts +0 -57
- package/src/native/shared/modal.ts +0 -157
- package/src/native/vertical-bars-web-search.ts +0 -80
- package/src/native/vertical-bars-web.ts +0 -411
- package/src/native-integration/transition.ts +0 -15
- package/src/vertical-bars.ts +0 -11
|
@@ -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
|
-
}
|
|
@@ -1,437 +0,0 @@
|
|
|
1
|
-
import Combine
|
|
2
|
-
import SwiftUI
|
|
3
|
-
import UIKit
|
|
4
|
-
|
|
5
|
-
protocol ShellVerticalBarsControlling: AnyObject {
|
|
6
|
-
var view: UIView { get }
|
|
7
|
-
func attach(to owner: UIViewController, in parent: UIView)
|
|
8
|
-
@MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
|
|
9
|
-
var ownsKeyboardChrome: Bool { get }
|
|
10
|
-
func detach()
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
@available(iOS 26.0, *)
|
|
14
|
-
final class ShellVerticalBarsModel: ObservableObject {
|
|
15
|
-
struct Item: Identifiable {
|
|
16
|
-
let id: String
|
|
17
|
-
let label: String
|
|
18
|
-
let accessibilityLabel: String
|
|
19
|
-
let image: UIImage?
|
|
20
|
-
let clear: Bool
|
|
21
|
-
let background: Color?
|
|
22
|
-
let borderColor: Color?
|
|
23
|
-
let borderWidth: CGFloat
|
|
24
|
-
let badge: ShellBadge?
|
|
25
|
-
let disabled: Bool
|
|
26
|
-
var selected: Bool
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
struct Group: Identifiable {
|
|
30
|
-
let id: String
|
|
31
|
-
let items: [Item]
|
|
32
|
-
let slot: ShellControl.ToolbarSlot?
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
@Published var search: ShellVerticalSearchModel?
|
|
36
|
-
@Published var back: Item?
|
|
37
|
-
@Published var groups: [Group] = []
|
|
38
|
-
@Published var tabs: [Item] = []
|
|
39
|
-
@Published var selection = ""
|
|
40
|
-
var activate: (String) -> Void = { _ in }
|
|
41
|
-
private var domSelection = ""
|
|
42
|
-
private var pendingSelection: ShellTabBar.PendingSelection?
|
|
43
|
-
private var pendingExpiryWork: DispatchWorkItem?
|
|
44
|
-
|
|
45
|
-
func apply(_ controls: [ShellControl], rendering: ShellRendering,
|
|
46
|
-
now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
|
|
47
|
-
func item(_ source: ShellItem) -> Item {
|
|
48
|
-
let image = rendering.image(source.content)
|
|
49
|
-
return Item(id: source.id, label: source.content.label,
|
|
50
|
-
accessibilityLabel: source.content.accessibilityLabel,
|
|
51
|
-
// UIKit toolbar extraction uses the UIImage's rendering mode.
|
|
52
|
-
image: source.content.disabled ? image?.withRenderingMode(.alwaysTemplate) : image,
|
|
53
|
-
clear: source.content.buttonFill == .clear,
|
|
54
|
-
background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
|
|
55
|
-
borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
|
|
56
|
-
borderWidth: source.content.borderWidth ?? 0,
|
|
57
|
-
badge: source.content.badge, disabled: source.content.disabled,
|
|
58
|
-
selected: source.content.selected)
|
|
59
|
-
}
|
|
60
|
-
withAnimation(.smooth(duration: 0.3)) {
|
|
61
|
-
back = controls.first(where: { $0.kind == .backButton })?.items.first.map(item)
|
|
62
|
-
groups = controls.compactMap { control in
|
|
63
|
-
guard [.button, .buttons, .menuButton].contains(control.kind) else { return nil }
|
|
64
|
-
return Group(id: control.id, items: control.items.map(item), slot: control.toolbarSlot)
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
tabs = controls.first(where: { $0.kind == .tabBar })?.items.map(item) ?? []
|
|
68
|
-
domSelection = tabs.first(where: \.selected)?.id ?? ""
|
|
69
|
-
reconcileSelection(now: now)
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
func select(_ id: String, now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent(), ttl: CFTimeInterval = 1) {
|
|
73
|
-
guard let item = tabs.first(where: { $0.id == id }), !item.disabled else { return }
|
|
74
|
-
pendingSelection = .init(id: id, until: now + ttl)
|
|
75
|
-
selection = id
|
|
76
|
-
pendingExpiryWork?.cancel()
|
|
77
|
-
let work = DispatchWorkItem { [weak self] in self?.reconcileSelection() }
|
|
78
|
-
pendingExpiryWork = work
|
|
79
|
-
DispatchQueue.main.asyncAfter(deadline: .now() + ttl + 0.02, execute: work)
|
|
80
|
-
activate(id)
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
private func reconcileSelection(now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
|
|
84
|
-
guard let pending = pendingSelection else { selection = domSelection; return }
|
|
85
|
-
let item = tabs.first(where: { $0.id == pending.id })
|
|
86
|
-
if item == nil || item!.disabled || now >= pending.until || domSelection == pending.id {
|
|
87
|
-
pendingSelection = nil
|
|
88
|
-
pendingExpiryWork?.cancel()
|
|
89
|
-
pendingExpiryWork = nil
|
|
90
|
-
selection = domSelection
|
|
91
|
-
} else {
|
|
92
|
-
selection = pending.id
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
@available(iOS 26.0, *)
|
|
98
|
-
private struct ShellVerticalBarsLabel: View {
|
|
99
|
-
let item: ShellVerticalBarsModel.Item
|
|
100
|
-
|
|
101
|
-
var body: some View {
|
|
102
|
-
if let image = item.image {
|
|
103
|
-
Label {
|
|
104
|
-
Text(item.label.isEmpty ? item.accessibilityLabel : item.label)
|
|
105
|
-
} icon: {
|
|
106
|
-
Image(uiImage: image)
|
|
107
|
-
}
|
|
108
|
-
} else {
|
|
109
|
-
Text(item.label)
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
@available(iOS 26.0, *)
|
|
115
|
-
@ViewBuilder
|
|
116
|
-
private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
|
|
117
|
-
let button = Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
|
|
118
|
-
.disabled(item.disabled)
|
|
119
|
-
.accessibilityLabel(item.accessibilityLabel)
|
|
120
|
-
.accessibilityIdentifier(item.id)
|
|
121
|
-
if let background = item.background {
|
|
122
|
-
button.buttonStyle(.glassProminent).tint(background)
|
|
123
|
-
} else {
|
|
124
|
-
button.overlay {
|
|
125
|
-
if let borderColor = item.borderColor {
|
|
126
|
-
Capsule().strokeBorder(borderColor, lineWidth: item.borderWidth).allowsHitTesting(false)
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
@available(iOS 26.0, *)
|
|
133
|
-
private struct ShellVerticalBarsButton: ToolbarContent {
|
|
134
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
135
|
-
let id: String
|
|
136
|
-
let placement: ToolbarItemPlacement
|
|
137
|
-
|
|
138
|
-
// Observe here so same-ID updates also refresh the shared toolbar background.
|
|
139
|
-
var body: some ToolbarContent {
|
|
140
|
-
if let item = model.groups.lazy.flatMap(\.items).first(where: { $0.id == id }) {
|
|
141
|
-
ToolbarItem(placement: placement) { verticalBarsButton(item, model: model) }
|
|
142
|
-
.sharedBackgroundVisibility(item.clear ? .hidden : .automatic)
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
@available(iOS 26.0, *)
|
|
148
|
-
private func verticalBarsBackButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
|
|
149
|
-
Button { model.activate(item.id) } label: { Image(systemName: "chevron.backward") }
|
|
150
|
-
.disabled(item.disabled)
|
|
151
|
-
.accessibilityLabel(item.accessibilityLabel)
|
|
152
|
-
.accessibilityIdentifier("BackButton")
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
@available(iOS 26.0, *)
|
|
156
|
-
private struct ShellVerticalBarsBadge: ViewModifier {
|
|
157
|
-
let badge: ShellBadge?
|
|
158
|
-
|
|
159
|
-
@ViewBuilder func body(content: Content) -> some View {
|
|
160
|
-
if let badge { content.badge(badge.value) }
|
|
161
|
-
else { content }
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
@available(iOS 26.0, *)
|
|
166
|
-
private struct ShellVerticalBarsView: View {
|
|
167
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
168
|
-
|
|
169
|
-
var body: some View {
|
|
170
|
-
Group {
|
|
171
|
-
if model.tabs.isEmpty {
|
|
172
|
-
ShellVerticalBarsPage(model: model)
|
|
173
|
-
} else {
|
|
174
|
-
TabView(selection: Binding(get: { model.selection }, set: { model.select($0) })) {
|
|
175
|
-
ForEach(model.tabs) { item in
|
|
176
|
-
ShellVerticalBarsPage(model: model)
|
|
177
|
-
.tag(item.id)
|
|
178
|
-
.tabItem { ShellVerticalBarsLabel(item: item) }
|
|
179
|
-
.modifier(ShellVerticalBarsBadge(badge: item.badge))
|
|
180
|
-
.disabled(item.disabled)
|
|
181
|
-
.accessibilityLabel(item.accessibilityLabel)
|
|
182
|
-
.accessibilityIdentifier(item.id)
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
.modifier(ShellVerticalBarsSearch(model: model))
|
|
188
|
-
.modifier(ShellVerticalBarsCompression())
|
|
189
|
-
.background(Color.clear)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
@available(iOS 26.0, *)
|
|
194
|
-
private struct ShellVerticalBarsPage: View {
|
|
195
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
196
|
-
|
|
197
|
-
var body: some View {
|
|
198
|
-
NavigationStack {
|
|
199
|
-
Color.clear.modifier(ShellVerticalBarsToolbarAdapter(model: model))
|
|
200
|
-
.toolbar {
|
|
201
|
-
if model.search?.configuration.available == true {
|
|
202
|
-
// Let the system adapt bottom-bar search to the vertical rail.
|
|
203
|
-
DefaultToolbarItem(kind: .search, placement: .bottomBar)
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
@available(iOS 26.0, *)
|
|
211
|
-
private struct ShellVerticalBarsSearch: ViewModifier {
|
|
212
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
213
|
-
@ViewBuilder func body(content: Content) -> some View {
|
|
214
|
-
if let search = model.search {
|
|
215
|
-
content.modifier(ShellVerticalSearchModifier(model: search))
|
|
216
|
-
} else { content }
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
@available(iOS 26.0, *)
|
|
221
|
-
private struct ShellVerticalBarsCompression: ViewModifier {
|
|
222
|
-
@ViewBuilder func body(content: Content) -> some View {
|
|
223
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
224
|
-
if #available(iOS 27.1, *) {
|
|
225
|
-
content.toolbarVerticalCompressionBehavior(.prefersToolbarItems)
|
|
226
|
-
} else {
|
|
227
|
-
content
|
|
228
|
-
}
|
|
229
|
-
#else
|
|
230
|
-
content
|
|
231
|
-
#endif
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
@available(iOS 26.0, *)
|
|
236
|
-
private struct ShellVerticalBarsToolbarAdapter: ViewModifier {
|
|
237
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
238
|
-
|
|
239
|
-
@ViewBuilder func body(content: Content) -> some View {
|
|
240
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
241
|
-
if #available(iOS 27.1, *) {
|
|
242
|
-
content.modifier(ShellVerticalBarsToolbar(model: model))
|
|
243
|
-
} else {
|
|
244
|
-
content.modifier(ShellVerticalBarsLegacyToolbar(model: model))
|
|
245
|
-
}
|
|
246
|
-
#else
|
|
247
|
-
content.modifier(ShellVerticalBarsLegacyToolbar(model: model))
|
|
248
|
-
#endif
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
253
|
-
@available(iOS 27.1, *)
|
|
254
|
-
private struct ShellVerticalBarsToolbar: ViewModifier {
|
|
255
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
256
|
-
|
|
257
|
-
func body(content: Content) -> some View {
|
|
258
|
-
content.toolbar {
|
|
259
|
-
if let back = model.back {
|
|
260
|
-
ToolbarItem(placement: .navigation) {
|
|
261
|
-
verticalBarsBackButton(back, model: model)
|
|
262
|
-
}
|
|
263
|
-
.axisBehavior(.verticalPreferred)
|
|
264
|
-
}
|
|
265
|
-
ForEach(model.groups.filter { $0.slot == .start }) { group in
|
|
266
|
-
if group.id != model.groups.first(where: { $0.slot == .start })?.id {
|
|
267
|
-
ToolbarSpacer(.fixed, placement: .topBarLeading)
|
|
268
|
-
}
|
|
269
|
-
ForEach(group.items) { item in
|
|
270
|
-
ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarLeading)
|
|
271
|
-
.axisBehavior(.verticalPreferred)
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
ForEach(model.groups.filter { $0.slot != .start }) { group in
|
|
275
|
-
if group.id != model.groups.first(where: { $0.slot != .start })?.id {
|
|
276
|
-
ToolbarSpacer(.fixed, placement: .topBarTrailing)
|
|
277
|
-
}
|
|
278
|
-
ForEach(group.items) { item in
|
|
279
|
-
ShellVerticalBarsButton(model: model, id: item.id, placement: .topBarTrailing)
|
|
280
|
-
.axisBehavior(.verticalPreferred)
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
#endif
|
|
287
|
-
|
|
288
|
-
@available(iOS 26.0, *)
|
|
289
|
-
private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
|
|
290
|
-
@ObservedObject var model: ShellVerticalBarsModel
|
|
291
|
-
|
|
292
|
-
func body(content: Content) -> some View {
|
|
293
|
-
content.toolbar {
|
|
294
|
-
if let back = model.back {
|
|
295
|
-
ToolbarItem(placement: .navigation) {
|
|
296
|
-
verticalBarsBackButton(back, model: model)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
ForEach(model.groups) { group in
|
|
300
|
-
if group.id != model.groups.first?.id {
|
|
301
|
-
ToolbarSpacer(.fixed, placement: .primaryAction)
|
|
302
|
-
}
|
|
303
|
-
ForEach(group.items) { item in
|
|
304
|
-
ShellVerticalBarsButton(model: model, id: item.id, placement: .primaryAction)
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
@available(iOS 26.0, *)
|
|
312
|
-
final class ShellVerticalBarsController: ShellVerticalBarsControlling {
|
|
313
|
-
private final class TransparentHostingController<Content: View>: UIHostingController<Content> {
|
|
314
|
-
var railEdge = "right"
|
|
315
|
-
var layoutSearch: (() -> Void)?
|
|
316
|
-
override func viewDidLayoutSubviews() {
|
|
317
|
-
super.viewDidLayoutSubviews()
|
|
318
|
-
makeFullSizeSurfacesTransparent(in: view)
|
|
319
|
-
layoutSearch?()
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
private func makeFullSizeSurfacesTransparent(in surface: UIView) {
|
|
323
|
-
let frame = surface.convert(surface.bounds, to: view)
|
|
324
|
-
guard !(surface is UIVisualEffectView) else { return }
|
|
325
|
-
let coversHost = frame.insetBy(dx: -1, dy: -1).contains(view.bounds)
|
|
326
|
-
// SwiftUI may add an opaque backing behind the rail controls, sized to
|
|
327
|
-
// the rail or to an expanded tab bar. Keep that backing clear without
|
|
328
|
-
// touching the glass controls or materials.
|
|
329
|
-
let coversRail = frame.minY <= 1 && frame.maxY >= view.bounds.maxY - 1 &&
|
|
330
|
-
(railEdge == "left" ? frame.minX <= 1 : frame.maxX >= view.bounds.maxX - 1)
|
|
331
|
-
if coversHost || coversRail {
|
|
332
|
-
surface.backgroundColor = .clear
|
|
333
|
-
surface.isOpaque = false
|
|
334
|
-
}
|
|
335
|
-
surface.subviews.forEach(makeFullSizeSurfacesTransparent)
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private final class RailContainer: UIView {
|
|
340
|
-
var railEdge = "right"
|
|
341
|
-
|
|
342
|
-
private var railWidth: CGFloat {
|
|
343
|
-
let inset = railEdge == "left" ? safeAreaInsets.left : safeAreaInsets.right
|
|
344
|
-
return inset > 0 ? inset : 80
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// The rail is not masked: expanded rail content (a widened tab bar) is
|
|
348
|
-
// allowed to draw over the WebView. Inside the base rail touches behave
|
|
349
|
-
// as before; beyond it touches are captured only where they land inside
|
|
350
|
-
// an actual bar surface so empty overlap still belongs to the WebView.
|
|
351
|
-
// The SwiftUI hosting scaffold reports a full-size hosting view for
|
|
352
|
-
// every point, so the hit result cannot tell bar content from empty
|
|
353
|
-
// space — the bar frames decide instead.
|
|
354
|
-
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
|
355
|
-
let hit = super.hitTest(point, with: event)
|
|
356
|
-
let inRail = railEdge == "left" ? point.x <= railWidth : point.x >= bounds.maxX - railWidth
|
|
357
|
-
if inRail { return hit }
|
|
358
|
-
// Expanded system search leaves the rail; its field and close button
|
|
359
|
-
// must receive touches instead of passing them through to the WebView.
|
|
360
|
-
var ancestor = hit
|
|
361
|
-
while let view = ancestor, view !== self {
|
|
362
|
-
if view is UIControl { return hit }
|
|
363
|
-
ancestor = view.superview
|
|
364
|
-
}
|
|
365
|
-
guard hit != nil, containsBarSurface(at: point) else { return nil }
|
|
366
|
-
return hit
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
private func containsBarSurface(at point: CGPoint) -> Bool {
|
|
370
|
-
containsBarSurface(in: self, at: point)
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
private func containsBarSurface(in view: UIView, at point: CGPoint) -> Bool {
|
|
374
|
-
guard !view.isHidden, view.alpha > 0.05 else { return false }
|
|
375
|
-
let name = NSStringFromClass(type(of: view))
|
|
376
|
-
if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar") || view is UISearchBar || view is UINavigationBar),
|
|
377
|
-
convert(view.bounds, from: view).contains(point) { return true }
|
|
378
|
-
return view.subviews.contains { containsBarSurface(in: $0, at: point) }
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
private let model = ShellVerticalBarsModel()
|
|
383
|
-
private lazy var controller = TransparentHostingController(rootView: ShellVerticalBarsView(model: model))
|
|
384
|
-
private let container = RailContainer()
|
|
385
|
-
private weak var owner: UIViewController?
|
|
386
|
-
private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
|
|
387
|
-
|
|
388
|
-
var view: UIView { container }
|
|
389
|
-
|
|
390
|
-
var ownsKeyboardChrome: Bool { model.search?.ownsKeyboardChrome == true }
|
|
391
|
-
|
|
392
|
-
init(activate: @escaping (String) -> Void,
|
|
393
|
-
changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
|
|
394
|
-
self.changed = changed
|
|
395
|
-
model.activate = activate
|
|
396
|
-
container.backgroundColor = .clear
|
|
397
|
-
controller.view.backgroundColor = .clear
|
|
398
|
-
controller.view.isOpaque = false
|
|
399
|
-
controller.layoutSearch = { [weak self] in self?.model.search?.configureField() }
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
func attach(to owner: UIViewController, in parent: UIView) {
|
|
403
|
-
guard self.owner !== owner || container.superview !== parent else { return }
|
|
404
|
-
detach()
|
|
405
|
-
self.owner = owner
|
|
406
|
-
owner.addChild(controller)
|
|
407
|
-
container.frame = parent.bounds
|
|
408
|
-
container.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
409
|
-
controller.view.frame = container.bounds
|
|
410
|
-
controller.view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
411
|
-
container.addSubview(controller.view)
|
|
412
|
-
parent.addSubview(container)
|
|
413
|
-
controller.didMove(toParent: owner)
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
@MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String) {
|
|
417
|
-
container.railEdge = edge
|
|
418
|
-
controller.railEdge = edge
|
|
419
|
-
let tabsAppear = model.tabs.isEmpty && controls.contains { $0.kind == .tabBar && !$0.items.isEmpty }
|
|
420
|
-
if let search = controls.first(where: { $0.search != nil })?.search {
|
|
421
|
-
if model.search?.configuration.id == search.id { model.search?.apply(search) }
|
|
422
|
-
else { model.search = ShellVerticalSearchModel(search, in: container, activate: model.activate, changed: changed) }
|
|
423
|
-
} else { model.search = nil }
|
|
424
|
-
model.apply(controls, rendering: rendering)
|
|
425
|
-
if tabsAppear { ShellCrossfade.enter(container, duration: ShellCrossfade.duration(180)) }
|
|
426
|
-
controller.overrideUserInterfaceStyle = controls.contains(where: \.dark) ? .dark : .light
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
func detach() {
|
|
430
|
-
guard controller.parent != nil else { return }
|
|
431
|
-
controller.willMove(toParent: nil)
|
|
432
|
-
container.removeFromSuperview()
|
|
433
|
-
controller.view.removeFromSuperview()
|
|
434
|
-
controller.removeFromParent()
|
|
435
|
-
owner = nil
|
|
436
|
-
}
|
|
437
|
-
}
|