@rdlabo/ionic-theme-ios27 1.2.0-1 → 1.2.0-3
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 +9 -0
- package/dist/css/components/ion-tabs.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.map +1 -1
- package/dist/index.js +31 -12
- package/dist/native/components/index.d.ts.map +1 -1
- package/dist/native/components/index.js +4 -1
- package/dist/native/components/ion-button.d.ts.map +1 -1
- package/dist/native/components/ion-button.js +3 -3
- package/dist/native/components/ion-buttons.d.ts.map +1 -1
- package/dist/native/components/ion-buttons.js +3 -2
- package/dist/native/components/searchable-tabs.d.ts.map +1 -1
- package/dist/native/components/searchable-tabs.js +6 -7
- package/dist/native/definitions.d.ts +8 -15
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/definitions.js +1 -6
- package/dist/native/index.d.ts +1 -2
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +64 -28
- package/dist/native/prehide.d.ts.map +1 -1
- package/dist/native/prehide.js +6 -4
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +40 -19
- package/dist/native/shared/candidate.d.ts.map +1 -1
- package/dist/native/shared/candidate.js +8 -0
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +11 -10
- package/dist/native/shared/modal.d.ts +8 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +143 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -1
- package/dist/native/vertical-bars-web.js +11 -3
- package/dist/sheets-of-glass/animations.d.ts +13 -4
- package/dist/sheets-of-glass/animations.d.ts.map +1 -1
- package/dist/sheets-of-glass/animations.js +56 -40
- package/dist/sheets-of-glass/index.d.ts +1 -1
- package/dist/sheets-of-glass/index.d.ts.map +1 -1
- package/dist/sheets-of-glass/index.js +64 -20
- package/dist/tab-bar-searchable/index.d.ts.map +1 -1
- package/dist/tab-bar-searchable/index.js +39 -11
- package/dist/vertical-bars.d.ts +1 -2
- package/dist/vertical-bars.d.ts.map +1 -1
- package/dist/vertical-bars.js +0 -1
- package/docs/iphone-duo-with-original-theme.md +37 -15
- package/docs/iphone-duo.md +90 -47
- package/docs/migration.md +3 -3
- package/docs/native-ui-shell.md +12 -8
- package/docs/special-markup.md +0 -13
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +1 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +50 -6
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalSearch.swift +118 -0
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +32 -179
- package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +5 -0
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +67 -0
- package/package.json +1 -1
- package/src/index.ts +40 -9
- package/src/native/components/index.ts +6 -1
- package/src/native/components/ion-button.ts +4 -3
- package/src/native/components/ion-buttons.ts +4 -2
- package/src/native/components/searchable-tabs.ts +8 -11
- package/src/native/definitions.ts +11 -19
- package/src/native/index.ts +67 -30
- package/src/native/prehide.ts +7 -4
- package/src/native/runtime.ts +40 -18
- package/src/native/shared/candidate.ts +10 -0
- package/src/native/shared/dom.ts +11 -10
- package/src/native/shared/modal.ts +157 -0
- package/src/native/vertical-bars-web.ts +12 -3
- package/src/sheets-of-glass/animations.ts +66 -41
- package/src/sheets-of-glass/index.ts +82 -16
- package/src/styles/components/ion-tabs.scss +7 -4
- package/src/styles/vertical-bars.scss +110 -35
- package/src/tab-bar-searchable/index.ts +40 -9
- package/src/vertical-bars.ts +1 -2
|
@@ -5,7 +5,8 @@ import UIKit
|
|
|
5
5
|
protocol ShellVerticalBarsControlling: AnyObject {
|
|
6
6
|
var view: UIView { get }
|
|
7
7
|
func attach(to owner: UIViewController, in parent: UIView)
|
|
8
|
-
func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
|
|
8
|
+
@MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String)
|
|
9
|
+
var ownsKeyboardChrome: Bool { get }
|
|
9
10
|
func detach()
|
|
10
11
|
}
|
|
11
12
|
|
|
@@ -16,6 +17,9 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
16
17
|
let label: String
|
|
17
18
|
let accessibilityLabel: String
|
|
18
19
|
let image: UIImage?
|
|
20
|
+
let background: Color?
|
|
21
|
+
let borderColor: Color?
|
|
22
|
+
let borderWidth: CGFloat
|
|
19
23
|
let badge: ShellBadge?
|
|
20
24
|
let disabled: Bool
|
|
21
25
|
var selected: Bool
|
|
@@ -27,6 +31,7 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
27
31
|
let slot: ShellControl.ToolbarSlot?
|
|
28
32
|
}
|
|
29
33
|
|
|
34
|
+
@Published var search: ShellVerticalSearchModel?
|
|
30
35
|
@Published var back: Item?
|
|
31
36
|
@Published var groups: [Group] = []
|
|
32
37
|
@Published var tabs: [Item] = []
|
|
@@ -41,7 +46,11 @@ final class ShellVerticalBarsModel: ObservableObject {
|
|
|
41
46
|
func item(_ source: ShellItem) -> Item {
|
|
42
47
|
Item(id: source.id, label: source.content.label,
|
|
43
48
|
accessibilityLabel: source.content.accessibilityLabel,
|
|
44
|
-
image: rendering.image(source.content),
|
|
49
|
+
image: rendering.image(source.content),
|
|
50
|
+
background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
|
|
51
|
+
borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
|
|
52
|
+
borderWidth: source.content.borderWidth ?? 0,
|
|
53
|
+
badge: source.content.badge, disabled: source.content.disabled,
|
|
45
54
|
selected: source.content.selected)
|
|
46
55
|
}
|
|
47
56
|
withAnimation(.smooth(duration: 0.3)) {
|
|
@@ -99,11 +108,21 @@ private struct ShellVerticalBarsLabel: View {
|
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
@available(iOS 26.0, *)
|
|
111
|
+
@ViewBuilder
|
|
102
112
|
private func verticalBarsButton(_ item: ShellVerticalBarsModel.Item, model: ShellVerticalBarsModel) -> some View {
|
|
103
|
-
Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
|
|
113
|
+
let button = Button { model.activate(item.id) } label: { ShellVerticalBarsLabel(item: item) }
|
|
104
114
|
.disabled(item.disabled)
|
|
105
115
|
.accessibilityLabel(item.accessibilityLabel)
|
|
106
116
|
.accessibilityIdentifier(item.id)
|
|
117
|
+
if let background = item.background {
|
|
118
|
+
button.buttonStyle(.glassProminent).tint(background)
|
|
119
|
+
} else {
|
|
120
|
+
button.overlay {
|
|
121
|
+
if let borderColor = item.borderColor {
|
|
122
|
+
Capsule().strokeBorder(borderColor, lineWidth: item.borderWidth).allowsHitTesting(false)
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
107
126
|
}
|
|
108
127
|
|
|
109
128
|
@available(iOS 26.0, *)
|
|
@@ -146,6 +165,7 @@ private struct ShellVerticalBarsView: View {
|
|
|
146
165
|
}
|
|
147
166
|
}
|
|
148
167
|
}
|
|
168
|
+
.modifier(ShellVerticalBarsSearch(model: model))
|
|
149
169
|
.modifier(ShellVerticalBarsCompression())
|
|
150
170
|
.background(Color.clear)
|
|
151
171
|
}
|
|
@@ -162,6 +182,16 @@ private struct ShellVerticalBarsPage: View {
|
|
|
162
182
|
}
|
|
163
183
|
}
|
|
164
184
|
|
|
185
|
+
@available(iOS 26.0, *)
|
|
186
|
+
private struct ShellVerticalBarsSearch: ViewModifier {
|
|
187
|
+
@ObservedObject var model: ShellVerticalBarsModel
|
|
188
|
+
@ViewBuilder func body(content: Content) -> some View {
|
|
189
|
+
if let search = model.search {
|
|
190
|
+
content.modifier(ShellVerticalSearchModifier(model: search))
|
|
191
|
+
} else { content }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
165
195
|
@available(iOS 26.0, *)
|
|
166
196
|
private struct ShellVerticalBarsCompression: ViewModifier {
|
|
167
197
|
@ViewBuilder func body(content: Content) -> some View {
|
|
@@ -254,9 +284,11 @@ private struct ShellVerticalBarsLegacyToolbar: ViewModifier {
|
|
|
254
284
|
final class ShellVerticalBarsController: ShellVerticalBarsControlling {
|
|
255
285
|
private final class TransparentHostingController<Content: View>: UIHostingController<Content> {
|
|
256
286
|
var railEdge = "right"
|
|
287
|
+
var layoutSearch: (() -> Void)?
|
|
257
288
|
override func viewDidLayoutSubviews() {
|
|
258
289
|
super.viewDidLayoutSubviews()
|
|
259
290
|
makeFullSizeSurfacesTransparent(in: view)
|
|
291
|
+
layoutSearch?()
|
|
260
292
|
}
|
|
261
293
|
|
|
262
294
|
private func makeFullSizeSurfacesTransparent(in surface: UIView) {
|
|
@@ -306,7 +338,7 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
|
|
|
306
338
|
private func containsBarSurface(in view: UIView, at point: CGPoint) -> Bool {
|
|
307
339
|
guard !view.isHidden, view.alpha > 0.05 else { return false }
|
|
308
340
|
let name = NSStringFromClass(type(of: view))
|
|
309
|
-
if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar")),
|
|
341
|
+
if (name.contains("TabBar") || name.contains("Platter") || name.contains("Pocket") || name.contains("Sidebar") || view is UISearchBar || view is UINavigationBar),
|
|
310
342
|
convert(view.bounds, from: view).contains(point) { return true }
|
|
311
343
|
return view.subviews.contains { containsBarSurface(in: $0, at: point) }
|
|
312
344
|
}
|
|
@@ -316,14 +348,20 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
|
|
|
316
348
|
private lazy var controller = TransparentHostingController(rootView: ShellVerticalBarsView(model: model))
|
|
317
349
|
private let container = RailContainer()
|
|
318
350
|
private weak var owner: UIViewController?
|
|
351
|
+
private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
|
|
319
352
|
|
|
320
353
|
var view: UIView { container }
|
|
321
354
|
|
|
322
|
-
|
|
355
|
+
var ownsKeyboardChrome: Bool { model.search?.ownsKeyboardChrome == true }
|
|
356
|
+
|
|
357
|
+
init(activate: @escaping (String) -> Void,
|
|
358
|
+
changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
|
|
359
|
+
self.changed = changed
|
|
323
360
|
model.activate = activate
|
|
324
361
|
container.backgroundColor = .clear
|
|
325
362
|
controller.view.backgroundColor = .clear
|
|
326
363
|
controller.view.isOpaque = false
|
|
364
|
+
controller.layoutSearch = { [weak self] in self?.model.search?.configureField() }
|
|
327
365
|
}
|
|
328
366
|
|
|
329
367
|
func attach(to owner: UIViewController, in parent: UIView) {
|
|
@@ -340,10 +378,16 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
|
|
|
340
378
|
controller.didMove(toParent: owner)
|
|
341
379
|
}
|
|
342
380
|
|
|
343
|
-
func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String) {
|
|
381
|
+
@MainActor func apply(_ controls: [ShellControl], rendering: ShellRendering, edge: String) {
|
|
344
382
|
container.railEdge = edge
|
|
345
383
|
controller.railEdge = edge
|
|
384
|
+
let tabsAppear = model.tabs.isEmpty && controls.contains { $0.kind == .tabBar && !$0.items.isEmpty }
|
|
385
|
+
if let search = controls.first(where: { $0.search != nil })?.search {
|
|
386
|
+
if model.search?.configuration.id == search.id { model.search?.apply(search) }
|
|
387
|
+
else { model.search = ShellVerticalSearchModel(search, in: container, activate: model.activate, changed: changed) }
|
|
388
|
+
} else { model.search = nil }
|
|
346
389
|
model.apply(controls, rendering: rendering)
|
|
390
|
+
if tabsAppear { ShellCrossfade.enter(container, duration: ShellCrossfade.duration(180)) }
|
|
347
391
|
controller.overrideUserInterfaceStyle = controls.contains(where: \.dark) ? .dark : .light
|
|
348
392
|
}
|
|
349
393
|
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import Combine
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 26.0, *)
|
|
6
|
+
final class ShellVerticalSearchModel: ObservableObject {
|
|
7
|
+
@Published private(set) var configuration: ShellSearch
|
|
8
|
+
@Published var text: String
|
|
9
|
+
@Published var presented: Bool
|
|
10
|
+
@Published var focused: Bool
|
|
11
|
+
private weak var railView: UIView?
|
|
12
|
+
private let activate: (String) -> Void
|
|
13
|
+
private let changed: (String, ShellSearchPhase, String, Bool, Int) -> Int
|
|
14
|
+
private let inputDelegate = ShellSearchInputDelegate()
|
|
15
|
+
private var editingSequence = 0
|
|
16
|
+
private var valueVersion: Int
|
|
17
|
+
|
|
18
|
+
init(_ configuration: ShellSearch, in railView: UIView,
|
|
19
|
+
activate: @escaping (String) -> Void,
|
|
20
|
+
changed: @escaping (String, ShellSearchPhase, String, Bool, Int) -> Int) {
|
|
21
|
+
self.configuration = configuration
|
|
22
|
+
self.railView = railView
|
|
23
|
+
self.activate = activate
|
|
24
|
+
self.changed = changed
|
|
25
|
+
text = configuration.value
|
|
26
|
+
presented = configuration.available && configuration.active
|
|
27
|
+
focused = configuration.focused
|
|
28
|
+
valueVersion = configuration.valueVersion
|
|
29
|
+
inputDelegate.clear = { [weak self] in self?.emit(.clear) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var ownsKeyboardChrome: Bool { presented && (focused || textField?.isFirstResponder == true) }
|
|
33
|
+
|
|
34
|
+
private var textField: UISearchTextField? {
|
|
35
|
+
func find(_ view: UIView) -> UISearchTextField? {
|
|
36
|
+
guard !view.isHidden, view.alpha > 0.01 else { return nil }
|
|
37
|
+
if let field = view as? UISearchTextField { return field }
|
|
38
|
+
for child in view.subviews {
|
|
39
|
+
if let field = find(child) { return field }
|
|
40
|
+
}
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
return railView.flatMap(find)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func apply(_ next: ShellSearch) {
|
|
47
|
+
let previous = configuration
|
|
48
|
+
configuration = next
|
|
49
|
+
if next.valueVersion != valueVersion || next.editSequence >= editingSequence {
|
|
50
|
+
valueVersion = next.valueVersion
|
|
51
|
+
text = next.value
|
|
52
|
+
}
|
|
53
|
+
presented = next.available && next.active
|
|
54
|
+
// The first Web acknowledgement still carries the old focus state.
|
|
55
|
+
if !next.active { focused = false }
|
|
56
|
+
else if previous.focused != next.focused { focused = next.focused }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func input(_ value: String) {
|
|
60
|
+
text = value
|
|
61
|
+
emit(.input)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func present(_ value: Bool) {
|
|
65
|
+
presented = value
|
|
66
|
+
if value != configuration.active {
|
|
67
|
+
activate(value ? configuration.trigger.id : configuration.closeId)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func focus(_ value: Bool) {
|
|
72
|
+
focused = value
|
|
73
|
+
emit(value ? .focus : .blur)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func commit() { emit(.commit) }
|
|
77
|
+
|
|
78
|
+
func configureField() {
|
|
79
|
+
guard let field = textField else { return }
|
|
80
|
+
field.accessibilityIdentifier = configuration.id
|
|
81
|
+
field.accessibilityLabel = configuration.field.accessibilityLabel
|
|
82
|
+
field.isEnabled = !configuration.disabled
|
|
83
|
+
field.autocapitalizationType = .none
|
|
84
|
+
field.autocorrectionType = .no
|
|
85
|
+
field.spellCheckingType = .no
|
|
86
|
+
field.clearButtonMode = .always
|
|
87
|
+
if field.delegate !== inputDelegate {
|
|
88
|
+
inputDelegate.original = field.delegate
|
|
89
|
+
field.delegate = inputDelegate
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func emit(_ phase: ShellSearchPhase) {
|
|
94
|
+
editingSequence = changed(configuration.id, phase, text,
|
|
95
|
+
textField?.markedTextRange != nil, valueVersion)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@available(iOS 26.0, *)
|
|
100
|
+
struct ShellVerticalSearchModifier: ViewModifier {
|
|
101
|
+
@ObservedObject var model: ShellVerticalSearchModel
|
|
102
|
+
@FocusState private var focused: Bool
|
|
103
|
+
|
|
104
|
+
@ViewBuilder func body(content: Content) -> some View {
|
|
105
|
+
if model.configuration.available {
|
|
106
|
+
content
|
|
107
|
+
.searchable(text: Binding(get: { model.text }, set: model.input),
|
|
108
|
+
isPresented: Binding(get: { model.presented }, set: model.present),
|
|
109
|
+
placement: .toolbar, prompt: Text(model.configuration.placeholder))
|
|
110
|
+
.searchToolbarBehavior(.minimize)
|
|
111
|
+
.searchPresentationToolbarBehavior(.avoidHidingContent)
|
|
112
|
+
.searchFocused($focused)
|
|
113
|
+
.onSubmit(of: .search, model.commit)
|
|
114
|
+
.onChange(of: focused) { _, value in model.focus(value) }
|
|
115
|
+
.onChange(of: model.focused) { _, value in focused = value }
|
|
116
|
+
} else { content }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -8,9 +8,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
8
8
|
public let jsName = "IonicNativeUIShell"
|
|
9
9
|
public let pluginMethods: [CAPPluginMethod] = [
|
|
10
10
|
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
|
|
11
|
-
CAPPluginMethod(name: "
|
|
12
|
-
CAPPluginMethod(name: "startDeviceLayoutMonitoring", returnType: CAPPluginReturnPromise),
|
|
13
|
-
CAPPluginMethod(name: "stopDeviceLayoutMonitoring", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "getWebViewMetrics", returnType: CAPPluginReturnPromise),
|
|
14
12
|
CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
|
|
15
13
|
CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise)
|
|
16
14
|
]
|
|
@@ -27,16 +25,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
27
25
|
private var pendingTabExpiryWorks: [String: DispatchWorkItem] = [:]
|
|
28
26
|
private var restoreTopEdge: (() -> Void)?
|
|
29
27
|
private var observers: [NSObjectProtocol] = []
|
|
30
|
-
private var
|
|
31
|
-
private var lastVerticalBarInset: CGFloat = 0
|
|
32
|
-
private var verticalBarPlacementObserved = false
|
|
33
|
-
private weak var observedVerticalBarView: UIView?
|
|
34
|
-
private var unregisterVerticalBarObservation: (() -> Void)?
|
|
35
|
-
private weak var observedHingeView: UIView?
|
|
36
|
-
private var hingeInteraction: UIInteraction?
|
|
37
|
-
private var hingeStatus: String?
|
|
38
|
-
private var deviceLayoutMonitoring = 0
|
|
39
|
-
private var lastDeviceLayout: String?
|
|
28
|
+
private var lastWebViewRadius: Double?
|
|
40
29
|
|
|
41
30
|
public override func load() {
|
|
42
31
|
for name in [UIApplication.didEnterBackgroundNotification, UIResponder.keyboardWillChangeFrameNotification, UIResponder.keyboardWillHideNotification,
|
|
@@ -57,7 +46,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
57
46
|
self.host?.isHidden = true
|
|
58
47
|
self.verticalBars?.view.isHidden = true
|
|
59
48
|
}
|
|
60
|
-
var searchOwnsKeyboard =
|
|
49
|
+
var searchOwnsKeyboard = self.verticalBars?.ownsKeyboardChrome == true
|
|
61
50
|
self.searchControllers.values.forEach { controller in
|
|
62
51
|
if controller.ownsKeyboardChrome { searchOwnsKeyboard = true }
|
|
63
52
|
if !keyboard { controller.surface.isHidden = true }
|
|
@@ -68,7 +57,6 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
68
57
|
}
|
|
69
58
|
} else if name == UIDevice.orientationDidChangeNotification {
|
|
70
59
|
self.notifyWebViewMetricsChange()
|
|
71
|
-
self.notifyVerticalBarPlacementChange()
|
|
72
60
|
}
|
|
73
61
|
})
|
|
74
62
|
}
|
|
@@ -78,8 +66,6 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
78
66
|
if name == UIResponder.keyboardDidHideNotification { self?.keyboardVisible = false }
|
|
79
67
|
self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIShellRefresh", data: name == UIApplication.didBecomeActiveNotification ? "{\"retireSearch\":true}" : "{}")
|
|
80
68
|
if name == UIApplication.didBecomeActiveNotification { self?.notifyWebViewMetricsChange() }
|
|
81
|
-
if name == UIApplication.didBecomeActiveNotification { self?.notifyVerticalBarPlacementChange() }
|
|
82
|
-
if name == UIApplication.didBecomeActiveNotification { self?.refreshHingeStatus() }
|
|
83
69
|
})
|
|
84
70
|
}
|
|
85
71
|
}
|
|
@@ -88,17 +74,6 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
88
74
|
observers.forEach(NotificationCenter.default.removeObserver)
|
|
89
75
|
}
|
|
90
76
|
|
|
91
|
-
private func stopDeviceLayoutObservation() {
|
|
92
|
-
unregisterVerticalBarObservation?()
|
|
93
|
-
unregisterVerticalBarObservation = nil
|
|
94
|
-
observedVerticalBarView = nil
|
|
95
|
-
if let interaction = hingeInteraction {
|
|
96
|
-
observedHingeView?.removeInteraction(interaction)
|
|
97
|
-
}
|
|
98
|
-
hingeInteraction = nil
|
|
99
|
-
observedHingeView = nil
|
|
100
|
-
}
|
|
101
|
-
|
|
102
77
|
private func webViewMetrics() -> JSObject? {
|
|
103
78
|
guard #available(iOS 26.0, *), let webView = bridge?.webView else { return nil }
|
|
104
79
|
webView.layoutIfNeeded()
|
|
@@ -106,158 +81,21 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
106
81
|
}
|
|
107
82
|
|
|
108
83
|
private func notifyWebViewMetricsChange() {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"webViewMetrics": webViewMetrics() ?? ["radius": 0]]
|
|
115
|
-
layout["hingeStatus"] = hingeStatus ?? NSNull()
|
|
116
|
-
return layout
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private func notifyDeviceLayoutChange() {
|
|
120
|
-
guard deviceLayoutMonitoring > 0 else { return }
|
|
121
|
-
let layout = deviceLayout()
|
|
122
|
-
let fingerprint = "\(verticalBarEdge() ?? "none"):\(verticalBarInset(for: verticalBarEdge())):\(hingeStatus ?? "none"):\(webViewMetrics()?["radius"] ?? 0)"
|
|
123
|
-
guard fingerprint != lastDeviceLayout else { return }
|
|
124
|
-
lastDeviceLayout = fingerprint
|
|
125
|
-
notifyListeners("deviceLayoutChange", data: layout)
|
|
84
|
+
let metrics = webViewMetrics() ?? ["radius": 0]
|
|
85
|
+
let radius = metrics["radius"] as? Double ?? 0
|
|
86
|
+
guard radius != lastWebViewRadius else { return }
|
|
87
|
+
lastWebViewRadius = radius
|
|
88
|
+
notifyListeners("webViewMetricsChange", data: metrics)
|
|
126
89
|
}
|
|
127
90
|
|
|
128
|
-
@objc func
|
|
91
|
+
@objc func getWebViewMetrics(_ call: CAPPluginCall) {
|
|
129
92
|
DispatchQueue.main.async { [weak self] in
|
|
130
|
-
self?.
|
|
131
|
-
self?.observeHingeStatus()
|
|
132
|
-
self?.refreshHingeStatus()
|
|
133
|
-
DispatchQueue.main.async {
|
|
134
|
-
call.resolve(self?.deviceLayout() ?? ["placement": ["edge": NSNull(), "inset": 0], "hingeStatus": NSNull(), "webViewMetrics": ["radius": 0]])
|
|
135
|
-
if self?.deviceLayoutMonitoring == 0 { self?.stopDeviceLayoutObservation() }
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
@objc func startDeviceLayoutMonitoring(_ call: CAPPluginCall) {
|
|
141
|
-
DispatchQueue.main.async { [weak self] in
|
|
142
|
-
self?.deviceLayoutMonitoring += 1
|
|
143
|
-
self?.lastDeviceLayout = nil
|
|
144
|
-
self?.observeVerticalBarPlacement()
|
|
145
|
-
self?.observeHingeStatus()
|
|
146
|
-
self?.refreshHingeStatus()
|
|
147
|
-
call.resolve()
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
@objc func stopDeviceLayoutMonitoring(_ call: CAPPluginCall) {
|
|
152
|
-
DispatchQueue.main.async { [weak self] in
|
|
153
|
-
if let self, self.deviceLayoutMonitoring > 0 {
|
|
154
|
-
self.deviceLayoutMonitoring -= 1
|
|
155
|
-
if self.deviceLayoutMonitoring == 0 {
|
|
156
|
-
self.lastDeviceLayout = nil
|
|
157
|
-
self.stopDeviceLayoutObservation()
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
call.resolve()
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Logical edge in the reading direction, matching UIVerticalBarEdge.
|
|
165
|
-
private func verticalBarEdge() -> String? {
|
|
166
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
167
|
-
if #available(iOS 27.1, *), let webView = bridge?.webView {
|
|
168
|
-
switch webView.traitCollection.verticalBarEdge {
|
|
169
|
-
case .leading: return "leading"
|
|
170
|
-
case .trailing: return "trailing"
|
|
171
|
-
default: break
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
#endif
|
|
175
|
-
// Toolchains older than the verticalBarEdge trait still expose the rail
|
|
176
|
-
// as a deep safe-area inset on the physical edge (iPhone Duo reserves
|
|
177
|
-
// ~80pt; ordinary iPhones stay below 70pt even in landscape).
|
|
178
|
-
guard let webView = bridge?.webView else { return nil }
|
|
179
|
-
webView.layoutIfNeeded()
|
|
180
|
-
let rtl = webView.effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
181
|
-
if webView.safeAreaInsets.right >= 70 { return rtl ? "leading" : "trailing" }
|
|
182
|
-
if webView.safeAreaInsets.left >= 70 { return rtl ? "trailing" : "leading" }
|
|
183
|
-
return nil
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
private func verticalBarInset(for edge: String?) -> CGFloat {
|
|
187
|
-
guard let edge, let webView = bridge?.webView else { return 0 }
|
|
188
|
-
webView.layoutIfNeeded()
|
|
189
|
-
let rtl = webView.effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
190
|
-
let physicalRight = (edge == "trailing") != rtl
|
|
191
|
-
return physicalRight ? webView.safeAreaInsets.right : webView.safeAreaInsets.left
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
private func verticalBarPlacement() -> JSObject {
|
|
195
|
-
if let edge = verticalBarEdge() { return ["edge": edge, "inset": Double(verticalBarInset(for: edge))] }
|
|
196
|
-
return ["edge": NSNull(), "inset": 0]
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
private func notifyVerticalBarPlacementChange() {
|
|
200
|
-
let edge = verticalBarEdge()
|
|
201
|
-
let inset = verticalBarInset(for: edge)
|
|
202
|
-
guard !verticalBarPlacementObserved || edge != lastVerticalBarEdge || abs(inset - lastVerticalBarInset) > 0.5 else { return }
|
|
203
|
-
verticalBarPlacementObserved = true
|
|
204
|
-
lastVerticalBarEdge = edge
|
|
205
|
-
lastVerticalBarInset = inset
|
|
206
|
-
notifyDeviceLayoutChange()
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private func observeVerticalBarPlacement() {
|
|
210
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
211
|
-
if #available(iOS 27.1, *), let webView = bridge?.webView, observedVerticalBarView !== webView {
|
|
212
|
-
unregisterVerticalBarObservation?()
|
|
213
|
-
observedVerticalBarView = webView
|
|
214
|
-
let traits: [UITrait] = [UITraitLayoutDirection.self] + UITraitCollection.systemTraitsAffectingVerticalBarEdge
|
|
215
|
-
let registration = webView.registerForTraitChanges(traits) { [weak self] (_: UIView, _: UITraitCollection) in
|
|
216
|
-
self?.notifyVerticalBarPlacementChange()
|
|
217
|
-
}
|
|
218
|
-
unregisterVerticalBarObservation = { [weak webView] in webView?.unregisterForTraitChanges(registration) }
|
|
93
|
+
call.resolve(self?.webViewMetrics() ?? ["radius": 0])
|
|
219
94
|
}
|
|
220
|
-
#endif
|
|
221
|
-
notifyVerticalBarPlacementChange()
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private func observeHingeStatus() {
|
|
225
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
226
|
-
if #available(iOS 27.1, *), let webView = bridge?.webView, observedHingeView !== webView {
|
|
227
|
-
if let interaction = hingeInteraction {
|
|
228
|
-
observedHingeView?.removeInteraction(interaction)
|
|
229
|
-
}
|
|
230
|
-
observedHingeView = webView
|
|
231
|
-
let interaction = UIHingeInteraction { [weak self] _, update in
|
|
232
|
-
let status: String?
|
|
233
|
-
switch update.hinge?.status {
|
|
234
|
-
case .closed: status = "closed"
|
|
235
|
-
case .partiallyOpen: status = "partiallyOpen"
|
|
236
|
-
case .fullyOpen: status = "fullyOpen"
|
|
237
|
-
default: status = nil
|
|
238
|
-
}
|
|
239
|
-
guard status != self?.hingeStatus else { return }
|
|
240
|
-
self?.hingeStatus = status
|
|
241
|
-
self?.notifyDeviceLayoutChange()
|
|
242
|
-
}
|
|
243
|
-
hingeInteraction = interaction
|
|
244
|
-
webView.addInteraction(interaction)
|
|
245
|
-
}
|
|
246
|
-
#endif
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private func refreshHingeStatus() {
|
|
250
|
-
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
251
|
-
if #available(iOS 27.1, *), let interaction = hingeInteraction as? UIHingeInteraction {
|
|
252
|
-
interaction.isEnabled = false
|
|
253
|
-
interaction.isEnabled = true
|
|
254
|
-
}
|
|
255
|
-
#endif
|
|
256
95
|
}
|
|
257
96
|
|
|
258
97
|
@objc func configure(_ call: CAPPluginCall) {
|
|
259
98
|
DispatchQueue.main.async { [weak self] in
|
|
260
|
-
self?.observeVerticalBarPlacement()
|
|
261
99
|
// A new JS context starts revision numbering again (live reload / navigation).
|
|
262
100
|
self?.restoreTopEdge?()
|
|
263
101
|
self?.restoreTopEdge = nil
|
|
@@ -352,10 +190,12 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
352
190
|
guard let snapshot = try? call.decode(ShellSnapshot.self), snapshot.isValid else {
|
|
353
191
|
call.reject("Invalid control snapshot"); return
|
|
354
192
|
}
|
|
193
|
+
self.notifyWebViewMetricsChange()
|
|
355
194
|
let duration = ShellCrossfade.duration(snapshot.transitionDuration)
|
|
356
195
|
let existing = Set(self.controls.keys)
|
|
357
196
|
let verticalBars = snapshot.controls.filter { $0.placement == .verticalBars }
|
|
358
197
|
let snapshots = snapshot.controls.filter { $0.placement != .verticalBars }
|
|
198
|
+
let verticalSearchActive = verticalBars.contains { $0.search.map { $0.available && $0.active } ?? false }
|
|
359
199
|
let width = snapshot.viewportWidth
|
|
360
200
|
self.revision = next
|
|
361
201
|
if snapshots.isEmpty && verticalBars.isEmpty {
|
|
@@ -371,14 +211,23 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
371
211
|
var fabs: [(ShellFab, ShellControl)] = []
|
|
372
212
|
var searches: [(ShellSearchControlling, ShellControl, CGRect, CGRect, UIView?, Bool)] = []
|
|
373
213
|
var rejectedSearches: [String] = []
|
|
374
|
-
if verticalBars.isEmpty || self.keyboardVisible {
|
|
214
|
+
if verticalBars.isEmpty || (self.keyboardVisible && !verticalSearchActive) {
|
|
375
215
|
self.verticalBars?.detach()
|
|
376
216
|
self.verticalBars = nil
|
|
377
217
|
if self.keyboardVisible { rejectedControls.append(contentsOf: verticalBars.map(\.id)) }
|
|
378
218
|
} else if let owner = self.bridge?.viewController {
|
|
379
|
-
let rail = self.verticalBars ?? ShellVerticalBarsController(
|
|
219
|
+
let rail = self.verticalBars ?? ShellVerticalBarsController(
|
|
220
|
+
activate: { [weak self] id in self?.activate(id) },
|
|
221
|
+
changed: { [weak self] id, phase, value, composing, valueVersion in
|
|
222
|
+
self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
|
|
223
|
+
})
|
|
380
224
|
self.verticalBars = rail
|
|
381
225
|
rail.attach(to: owner, in: owner.view)
|
|
226
|
+
if let frame = snapshot.verticalBarFrame {
|
|
227
|
+
rail.view.frame = webView.convert(frame.rect.applying(CGAffineTransform(scaleX: scale, y: scale)), to: owner.view)
|
|
228
|
+
} else {
|
|
229
|
+
rail.view.frame = owner.view.bounds
|
|
230
|
+
}
|
|
382
231
|
rail.apply(verticalBars, rendering: self.rendering, edge: snapshot.verticalBarEdge ?? "right")
|
|
383
232
|
rail.view.isHidden = false
|
|
384
233
|
} else {
|
|
@@ -430,11 +279,7 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
430
279
|
controller = ShellSearchController()
|
|
431
280
|
controller.activate = { [weak self] id in self?.activate(id) }
|
|
432
281
|
controller.changed = { [weak self] id, phase, value, composing, valueVersion in
|
|
433
|
-
|
|
434
|
-
self.sequence += 1
|
|
435
|
-
self.notifyListeners("search", data: ["id": id, "phase": phase.rawValue, "value": value,
|
|
436
|
-
"composing": composing, "valueVersion": valueVersion, "sequence": self.sequence, "revision": self.revision])
|
|
437
|
-
return self.sequence
|
|
282
|
+
self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
|
|
438
283
|
}
|
|
439
284
|
controller.attach(to: owner, in: owner.view)
|
|
440
285
|
self.searchControllers[id] = controller
|
|
@@ -535,6 +380,14 @@ public class IonicNativeUIShellPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDele
|
|
|
535
380
|
activate(itemId)
|
|
536
381
|
}
|
|
537
382
|
|
|
383
|
+
private func searchChanged(_ id: String, phase: ShellSearchPhase, value: String,
|
|
384
|
+
composing: Bool, valueVersion: Int) -> Int {
|
|
385
|
+
sequence += 1
|
|
386
|
+
notifyListeners("search", data: ["id": id, "phase": phase.rawValue, "value": value,
|
|
387
|
+
"composing": composing, "valueVersion": valueVersion, "sequence": sequence, "revision": revision])
|
|
388
|
+
return sequence
|
|
389
|
+
}
|
|
390
|
+
|
|
538
391
|
private func activate(_ id: String) {
|
|
539
392
|
sequence += 1
|
|
540
393
|
notifyListeners("activate", data: ["id": id, "revision": revision, "sequence": sequence])
|
|
@@ -11,11 +11,13 @@ struct ShellSnapshot: Decodable {
|
|
|
11
11
|
let revision: Int
|
|
12
12
|
let transitionDuration: Double?
|
|
13
13
|
let viewportWidth: Double
|
|
14
|
+
let verticalBarFrame: ShellFrame?
|
|
14
15
|
let verticalBarEdge: String?
|
|
15
16
|
let controls: [ShellControl]
|
|
16
17
|
|
|
17
18
|
var isValid: Bool {
|
|
18
19
|
revision >= 0 && (transitionDuration.map { $0.isFinite && $0 >= 0 && $0 <= 500 } ?? true) && viewportWidth.isFinite && viewportWidth > 0 && controls.count <= 100 &&
|
|
20
|
+
(verticalBarFrame.map { $0.isValid } ?? true) &&
|
|
19
21
|
(verticalBarEdge == nil || verticalBarEdge == "left" || verticalBarEdge == "right") &&
|
|
20
22
|
Set(controls.map(\.id)).count == controls.count && controls.allSatisfy(\.isValid)
|
|
21
23
|
}
|
|
@@ -105,6 +107,9 @@ struct ShellItemContent: Decodable, Equatable {
|
|
|
105
107
|
let fontSize: Double
|
|
106
108
|
let fontWeight: Double
|
|
107
109
|
let color: String
|
|
110
|
+
let backgroundColor: String?
|
|
111
|
+
let borderColor: String?
|
|
112
|
+
let borderWidth: CGFloat?
|
|
108
113
|
let badge: ShellBadge?
|
|
109
114
|
var icon: String?
|
|
110
115
|
var iconWidth: Double?
|