@rdlabo/capacitor-native-ui 0.0.0-stage → 1.2.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/LICENSE +21 -0
- package/Package.swift +23 -0
- package/README.md +53 -2
- package/RdlaboCapacitorNativeUi.podspec +17 -0
- package/android/build.gradle +69 -0
- package/android/proguard-rules.pro +0 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUI.kt +51 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUIPlugin.kt +192 -0
- package/android/src/main/java/dev/rdlabo/capacitor/nativeui/NativeUISnapshot.kt +57 -0
- package/dist/_virtual/_rolldown/runtime.js +13 -0
- package/dist/core.d.ts +4 -0
- package/dist/core.js +3 -0
- package/dist/native/activation.d.ts +14 -0
- package/dist/native/activation.d.ts.map +1 -0
- package/dist/native/activation.js +305 -0
- package/dist/native/activation.js.map +1 -0
- package/dist/native/components/index.d.ts +19 -0
- package/dist/native/components/index.d.ts.map +1 -0
- package/dist/native/components/index.js +38 -0
- package/dist/native/components/index.js.map +1 -0
- package/dist/native/components/ion-back-button.d.ts +10 -0
- package/dist/native/components/ion-back-button.d.ts.map +1 -0
- package/dist/native/components/ion-back-button.js +21 -0
- package/dist/native/components/ion-back-button.js.map +1 -0
- package/dist/native/components/ion-button.d.ts +10 -0
- package/dist/native/components/ion-button.d.ts.map +1 -0
- package/dist/native/components/ion-button.js +25 -0
- package/dist/native/components/ion-button.js.map +1 -0
- package/dist/native/components/ion-buttons.d.ts +11 -0
- package/dist/native/components/ion-buttons.d.ts.map +1 -0
- package/dist/native/components/ion-buttons.js +30 -0
- package/dist/native/components/ion-buttons.js.map +1 -0
- package/dist/native/components/ion-fab.d.ts +10 -0
- package/dist/native/components/ion-fab.d.ts.map +1 -0
- package/dist/native/components/ion-fab.js +132 -0
- package/dist/native/components/ion-fab.js.map +1 -0
- package/dist/native/components/ion-menu-button.d.ts +13 -0
- package/dist/native/components/ion-menu-button.d.ts.map +1 -0
- package/dist/native/components/ion-menu-button.js +44 -0
- package/dist/native/components/ion-menu-button.js.map +1 -0
- package/dist/native/components/ion-segment.d.ts +11 -0
- package/dist/native/components/ion-segment.d.ts.map +1 -0
- package/dist/native/components/ion-segment.js +30 -0
- package/dist/native/components/ion-segment.js.map +1 -0
- package/dist/native/components/ion-tab-bar.d.ts +11 -0
- package/dist/native/components/ion-tab-bar.d.ts.map +1 -0
- package/dist/native/components/ion-tab-bar.js +50 -0
- package/dist/native/components/ion-tab-bar.js.map +1 -0
- package/dist/native/components/searchable-tabs.d.ts +20 -0
- package/dist/native/components/searchable-tabs.d.ts.map +1 -0
- package/dist/native/components/searchable-tabs.js +413 -0
- package/dist/native/components/searchable-tabs.js.map +1 -0
- package/dist/native/definitions.d.ts +255 -0
- package/dist/native/definitions.d.ts.map +1 -0
- package/dist/native/definitions.js +0 -0
- package/dist/native/extensions.d.ts +31 -0
- package/dist/native/extensions.d.ts.map +1 -0
- package/dist/native/extensions.js +122 -0
- package/dist/native/extensions.js.map +1 -0
- package/dist/native/foldable/fold-state.d.ts +5 -0
- package/dist/native/foldable/fold-state.d.ts.map +1 -0
- package/dist/native/foldable/fold-state.js +15 -0
- package/dist/native/foldable/fold-state.js.map +1 -0
- package/dist/native/foldable/types.d.ts +12 -0
- package/dist/native/foldable/types.d.ts.map +1 -0
- package/dist/native/foldable/types.js +0 -0
- package/dist/native/html.d.ts +9 -0
- package/dist/native/html.d.ts.map +1 -0
- package/dist/native/html.js +188 -0
- package/dist/native/html.js.map +1 -0
- package/dist/native/index.d.ts +10 -0
- package/dist/native/index.d.ts.map +1 -0
- package/dist/native/index.js +28 -0
- package/dist/native/index.js.map +1 -0
- package/dist/native/legacy-layout.d.ts +5 -0
- package/dist/native/legacy-layout.d.ts.map +1 -0
- package/dist/native/legacy-layout.js +27 -0
- package/dist/native/legacy-layout.js.map +1 -0
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +285 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +9 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +53 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +12 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +110 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +11 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +167 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts +8 -0
- package/dist/native/prehide.d.ts.map +1 -0
- package/dist/native/prehide.js +238 -0
- package/dist/native/prehide.js.map +1 -0
- package/dist/native/runtime.d.ts +7 -0
- package/dist/native/runtime.d.ts.map +1 -0
- package/dist/native/runtime.js +788 -0
- package/dist/native/runtime.js.map +1 -0
- package/dist/native/shared/bridge.d.ts +6 -0
- package/dist/native/shared/bridge.d.ts.map +1 -0
- package/dist/native/shared/bridge.js +18 -0
- package/dist/native/shared/bridge.js.map +1 -0
- package/dist/native/shared/candidate.d.ts +25 -0
- package/dist/native/shared/candidate.d.ts.map +1 -0
- package/dist/native/shared/candidate.js +106 -0
- package/dist/native/shared/candidate.js.map +1 -0
- package/dist/native/shared/clipping.d.ts +5 -0
- package/dist/native/shared/clipping.d.ts.map +1 -0
- package/dist/native/shared/clipping.js +26 -0
- package/dist/native/shared/clipping.js.map +1 -0
- package/dist/native/shared/crossfade.d.ts +12 -0
- package/dist/native/shared/crossfade.d.ts.map +1 -0
- package/dist/native/shared/crossfade.js +67 -0
- package/dist/native/shared/crossfade.js.map +1 -0
- package/dist/native/shared/diagnostics.d.ts +4 -0
- package/dist/native/shared/diagnostics.d.ts.map +1 -0
- package/dist/native/shared/diagnostics.js +18 -0
- package/dist/native/shared/diagnostics.js.map +1 -0
- package/dist/native/shared/dom.d.ts +46 -0
- package/dist/native/shared/dom.d.ts.map +1 -0
- package/dist/native/shared/dom.js +180 -0
- package/dist/native/shared/dom.js.map +1 -0
- package/dist/native/shared/events.d.ts +15 -0
- package/dist/native/shared/events.d.ts.map +1 -0
- package/dist/native/shared/events.js +17 -0
- package/dist/native/shared/events.js.map +1 -0
- package/dist/native/shared/icons.d.ts +8 -0
- package/dist/native/shared/icons.d.ts.map +1 -0
- package/dist/native/shared/icons.js +61 -0
- package/dist/native/shared/icons.js.map +1 -0
- package/dist/native/shared/modal.d.ts +10 -0
- package/dist/native/shared/modal.d.ts.map +1 -0
- package/dist/native/shared/modal.js +160 -0
- package/dist/native/shared/modal.js.map +1 -0
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/shared/toolbar-options.d.ts +9 -0
- package/dist/native/shared/toolbar-options.d.ts.map +1 -0
- package/dist/native/shared/toolbar-options.js +36 -0
- package/dist/native/shared/toolbar-options.js.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts +10 -0
- package/dist/native/shared/vertical-bars-geometry.d.ts.map +1 -0
- package/dist/native/shared/vertical-bars-geometry.js +85 -0
- package/dist/native/shared/vertical-bars-geometry.js.map +1 -0
- package/dist/native/startup.d.ts +8 -0
- package/dist/native/startup.d.ts.map +1 -0
- package/dist/native/startup.js +86 -0
- package/dist/native/startup.js.map +1 -0
- package/dist/native/vertical-bars-web-search.d.ts +10 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +90 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts +5 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -0
- package/dist/native/vertical-bars-web.js +421 -0
- package/dist/native/vertical-bars-web.js.map +1 -0
- package/dist/native-integration/index.d.ts +25 -0
- package/dist/native-integration/index.d.ts.map +1 -0
- package/dist/native-integration/index.js +33 -0
- package/dist/native-integration/index.js.map +1 -0
- package/dist/vertical-bars.d.ts +6 -0
- package/dist/vertical-bars.js +5 -0
- package/docs/configuration.md +187 -0
- package/docs/framework-independent.md +82 -0
- package/docs/migration.md +206 -0
- package/docs/native-mini-player.md +47 -0
- package/docs/native-ui-extensions.md +365 -0
- package/docs/native-ui.md +409 -0
- package/docs/setup.md +205 -0
- package/docs/vertical-bars.md +379 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButton.swift +71 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISearchController.swift +439 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUITabBar.swift +218 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalBars.swift +590 -0
- package/ios/Sources/NativeUIPlugin/Components/NativeUIVerticalSearch.swift +118 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUI.swift +178 -0
- package/ios/Sources/NativeUIPlugin/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/NativeUIPlugin/NativeUIPlugin.swift +615 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayController.swift +119 -0
- package/ios/Sources/NativeUIPlugin/Overlays/NativeUIOverlayHost.swift +75 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUISnapshot.swift +161 -0
- package/ios/Sources/NativeUIPlugin/Shared/NativeUIVerticalBarsGeometry.swift +52 -0
- package/ios/Tests/NativeUIPluginTests/NativeUIExtensionTests.swift +165 -0
- package/ios/Tests/NativeUIPluginTests/NativeUISnapshotTests.swift +805 -0
- package/package.json +89 -4
- package/src/core.ts +15 -0
- package/src/native/activation.ts +394 -0
- package/src/native/components/index.ts +45 -0
- package/src/native/components/ion-back-button.ts +22 -0
- package/src/native/components/ion-button.ts +25 -0
- package/src/native/components/ion-buttons.ts +37 -0
- package/src/native/components/ion-fab.ts +151 -0
- package/src/native/components/ion-menu-button.ts +37 -0
- package/src/native/components/ion-segment.ts +27 -0
- package/src/native/components/ion-tab-bar.ts +50 -0
- package/src/native/components/searchable-tabs.ts +499 -0
- package/src/native/definitions.ts +324 -0
- package/src/native/extensions.ts +176 -0
- package/src/native/foldable/fold-state.ts +10 -0
- package/src/native/foldable/types.ts +4 -0
- package/src/native/html.ts +193 -0
- package/src/native/index.ts +41 -0
- package/src/native/legacy-layout.ts +21 -0
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +349 -0
- package/src/native/overlays/dialog-animation.ts +53 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +120 -0
- package/src/native/overlays/styles.ts +181 -0
- package/src/native/prehide.ts +285 -0
- package/src/native/runtime.ts +988 -0
- package/src/native/shared/bridge.ts +18 -0
- package/src/native/shared/candidate.ts +162 -0
- package/src/native/shared/clipping.ts +24 -0
- package/src/native/shared/crossfade.ts +67 -0
- package/src/native/shared/diagnostics.ts +13 -0
- package/src/native/shared/dom.ts +304 -0
- package/src/native/shared/events.ts +14 -0
- package/src/native/shared/icons.ts +57 -0
- package/src/native/shared/modal.ts +157 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/shared/toolbar-options.ts +53 -0
- package/src/native/shared/vertical-bars-geometry.ts +80 -0
- package/src/native/startup.ts +84 -0
- package/src/native/vertical-bars-web-search.ts +86 -0
- package/src/native/vertical-bars-web.ts +498 -0
- package/src/native-integration/index.ts +49 -0
- package/src/vertical-bars.ts +11 -0
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import Capacitor
|
|
2
|
+
import UIKit
|
|
3
|
+
import SwiftUI
|
|
4
|
+
import WebKit
|
|
5
|
+
|
|
6
|
+
@objc(NativeUIPlugin)
|
|
7
|
+
public class NativeUIPlugin: CAPPlugin, CAPBridgedPlugin, UITabBarDelegate {
|
|
8
|
+
public let identifier = "NativeUIPlugin"
|
|
9
|
+
public let jsName = "NativeUI"
|
|
10
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
11
|
+
CAPPluginMethod(name: "completeExtensionAction", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "getWebViewMetrics", returnType: CAPPluginReturnPromise),
|
|
14
|
+
CAPPluginMethod(name: "getVerticalBarsGeometry", returnType: CAPPluginReturnPromise),
|
|
15
|
+
CAPPluginMethod(name: "update", returnType: CAPPluginReturnPromise),
|
|
16
|
+
CAPPluginMethod(name: "clear", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "prepareOverlay", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "presentOverlay", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "dismissOverlay", returnType: CAPPluginReturnPromise),
|
|
20
|
+
CAPPluginMethod(name: "closeOverlay", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "stopOverlays", returnType: CAPPluginReturnPromise)
|
|
22
|
+
]
|
|
23
|
+
private var overlays: NativeUIOverlayController?
|
|
24
|
+
private var host: NativeUIHost?
|
|
25
|
+
private var verticalBars: NativeUIVerticalBarsControlling?
|
|
26
|
+
private var controls: [String: UIView] = [:]
|
|
27
|
+
private var searchControllers: [String: NativeUISearchControlling] = [:]
|
|
28
|
+
private var extensionControllers: [String: NativeUIExtensionController] = [:]
|
|
29
|
+
private var extensionSessions: [String: UUID] = [:]
|
|
30
|
+
private var extensionNodes: [String: NativeUIControl] = [:]
|
|
31
|
+
private let extensionActions = NativeUIExtensionActions()
|
|
32
|
+
private var fingerprints: [String: NativeUIControl] = [:]
|
|
33
|
+
private let rendering = NativeUIControlRendering()
|
|
34
|
+
private var configurationGeneration = 0
|
|
35
|
+
private var revision = 0
|
|
36
|
+
private var sequence = 0
|
|
37
|
+
private var keyboardVisible = false
|
|
38
|
+
private var pendingTabSelections: [String: NativeUITabBar.PendingSelection] = [:]
|
|
39
|
+
private var pendingTabExpiryWorks: [String: DispatchWorkItem] = [:]
|
|
40
|
+
private var restoreTopEdge: (() -> Void)?
|
|
41
|
+
private var observers: [NSObjectProtocol] = []
|
|
42
|
+
private var lastWebViewRadius: Double?
|
|
43
|
+
private var lastVerticalBarEdge: String?
|
|
44
|
+
|
|
45
|
+
public override func load() {
|
|
46
|
+
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
47
|
+
if #available(iOS 27.1, *), let webView = bridge?.webView {
|
|
48
|
+
webView.registerForTraitChanges(UITraitCollection.systemTraitsAffectingVerticalBarEdge) { [weak self] (_: WKWebView, _: UITraitCollection) in
|
|
49
|
+
self?.notifyWebViewMetricsChange()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
#endif
|
|
53
|
+
for name in [UIApplication.didEnterBackgroundNotification, UIResponder.keyboardWillChangeFrameNotification, UIResponder.keyboardWillHideNotification,
|
|
54
|
+
UIDevice.orientationDidChangeNotification] {
|
|
55
|
+
observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] notification in
|
|
56
|
+
guard let self else { return }
|
|
57
|
+
let keyboard = name == UIResponder.keyboardWillChangeFrameNotification || name == UIResponder.keyboardWillHideNotification
|
|
58
|
+
if keyboard {
|
|
59
|
+
if name == UIResponder.keyboardWillHideNotification { self.keyboardVisible = false }
|
|
60
|
+
else if let window = self.bridge?.webView?.window,
|
|
61
|
+
let frame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect {
|
|
62
|
+
let local = window.convert(frame, from: window.screen.coordinateSpace)
|
|
63
|
+
let overlap = window.bounds.intersection(local)
|
|
64
|
+
self.keyboardVisible = !overlap.isNull && overlap.width > 0 && overlap.height > 0
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if !keyboard {
|
|
68
|
+
self.host?.isHidden = true
|
|
69
|
+
self.verticalBars?.view.isHidden = true
|
|
70
|
+
self.extensionControllers.values.forEach { $0.view.isHidden = true }
|
|
71
|
+
}
|
|
72
|
+
var searchOwnsKeyboard = self.verticalBars?.ownsKeyboardChrome == true
|
|
73
|
+
self.searchControllers.values.forEach { controller in
|
|
74
|
+
if controller.ownsKeyboardChrome { searchOwnsKeyboard = true }
|
|
75
|
+
if !keyboard { controller.surface.isHidden = true }
|
|
76
|
+
}
|
|
77
|
+
if keyboard {
|
|
78
|
+
if !searchOwnsKeyboard {
|
|
79
|
+
self.bridge?.triggerWindowJSEvent(eventName: "nativeUIRefresh")
|
|
80
|
+
}
|
|
81
|
+
} else if name == UIDevice.orientationDidChangeNotification {
|
|
82
|
+
self.notifyWebViewMetricsChange()
|
|
83
|
+
}
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
for name in [UIApplication.didBecomeActiveNotification, UIResponder.keyboardDidHideNotification,
|
|
87
|
+
UIDevice.orientationDidChangeNotification] {
|
|
88
|
+
observers.append(NotificationCenter.default.addObserver(forName: name, object: nil, queue: .main) { [weak self] _ in
|
|
89
|
+
if name == UIResponder.keyboardDidHideNotification { self?.keyboardVisible = false }
|
|
90
|
+
self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIRefresh", data: name == UIApplication.didBecomeActiveNotification ? "{\"retireSearch\":true}" : "{}")
|
|
91
|
+
if name == UIApplication.didBecomeActiveNotification { self?.notifyWebViewMetricsChange() }
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
deinit {
|
|
97
|
+
observers.forEach(NotificationCenter.default.removeObserver)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@objc func prepareOverlay(_ call: CAPPluginCall) {
|
|
101
|
+
DispatchQueue.main.async { [weak self] in
|
|
102
|
+
guard let self, let id = call.getString("id"),
|
|
103
|
+
let source = self.bridge?.webView, let owner = self.bridge?.viewController else {
|
|
104
|
+
call.reject("Popover source unavailable"); return
|
|
105
|
+
}
|
|
106
|
+
if self.overlays == nil {
|
|
107
|
+
self.overlays = NativeUIOverlayController(source: source, owner: owner) { [weak self] id, action in
|
|
108
|
+
self?.notifyListeners("overlay", data: ["id": id, "action": action])
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
guard self.overlays?.prepare(id) == true else { call.reject("Popover already pending"); return }
|
|
112
|
+
call.resolve()
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@objc func presentOverlay(_ call: CAPPluginCall) {
|
|
117
|
+
DispatchQueue.main.async { [weak self] in
|
|
118
|
+
guard let self, let overlays = self.overlays, let id = call.getString("id") else {
|
|
119
|
+
call.reject("Popover not prepared"); return
|
|
120
|
+
}
|
|
121
|
+
overlays.present(id, options: call.getObject("presentation"), anchored: { [weak self] anchor, host, completion in
|
|
122
|
+
guard let self, let rail = self.verticalBars,
|
|
123
|
+
rail.presentPopover(anchor: anchor, host: host, completion: completion) else { return false }
|
|
124
|
+
let cancel = host.cancelAnchored
|
|
125
|
+
host.cancelAnchored = { [weak self, weak rail] in
|
|
126
|
+
cancel?()
|
|
127
|
+
// Cancellation removed the rail; invalidate native ownership even
|
|
128
|
+
// if the next DOM snapshot is identical to the previous one.
|
|
129
|
+
if let self, let rail, self.verticalBars === rail { self.verticalBars = nil }
|
|
130
|
+
self?.bridge?.triggerWindowJSEvent(eventName: "nativeUIRefresh")
|
|
131
|
+
}
|
|
132
|
+
return true
|
|
133
|
+
}) { presented in
|
|
134
|
+
if presented { call.resolve() } else { call.reject("Native Vertical Bars anchor unavailable") }
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@objc func dismissOverlay(_ call: CAPPluginCall) {
|
|
140
|
+
DispatchQueue.main.async { [weak self] in
|
|
141
|
+
guard let overlays = self?.overlays, let id = call.getString("id") else { call.resolve(); return }
|
|
142
|
+
overlays.dismiss(id, animated: call.getBool("animated") ?? false) { call.resolve() }
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@objc func closeOverlay(_ call: CAPPluginCall) {
|
|
147
|
+
DispatchQueue.main.async { [weak self] in
|
|
148
|
+
guard let overlays = self?.overlays, let id = call.getString("id") else { call.resolve(); return }
|
|
149
|
+
overlays.close(id) { call.resolve() }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@objc func stopOverlays(_ call: CAPPluginCall) {
|
|
154
|
+
DispatchQueue.main.async { [weak self] in
|
|
155
|
+
guard let self, let overlays = self.overlays else { call.resolve(); return }
|
|
156
|
+
overlays.stop {
|
|
157
|
+
if self.overlays === overlays { self.overlays = nil }
|
|
158
|
+
call.resolve()
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func webViewMetrics() -> JSObject? {
|
|
164
|
+
guard #available(iOS 26.0, *), let webView = bridge?.webView else { return nil }
|
|
165
|
+
webView.layoutIfNeeded()
|
|
166
|
+
var metrics: JSObject = ["radius": Double(webView.effectiveRadius(corner: .topLeft)), "verticalBarEdge": NSNull()]
|
|
167
|
+
#if canImport(UIKit, _underlyingVersion: 9127.0.85) && !targetEnvironment(macCatalyst)
|
|
168
|
+
if #available(iOS 27.1, *) {
|
|
169
|
+
switch webView.traitCollection.verticalBarEdge {
|
|
170
|
+
case .leading: metrics["verticalBarEdge"] = "leading"
|
|
171
|
+
case .trailing: metrics["verticalBarEdge"] = "trailing"
|
|
172
|
+
default: break
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
#endif
|
|
176
|
+
return metrics
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private func notifyWebViewMetricsChange() {
|
|
180
|
+
let metrics = webViewMetrics() ?? ["radius": 0]
|
|
181
|
+
let radius = metrics["radius"] as? Double ?? 0
|
|
182
|
+
let edge = metrics["verticalBarEdge"] as? String
|
|
183
|
+
guard radius != lastWebViewRadius || edge != lastVerticalBarEdge else { return }
|
|
184
|
+
lastVerticalBarEdge = edge
|
|
185
|
+
lastWebViewRadius = radius
|
|
186
|
+
notifyListeners("webViewMetricsChange", data: metrics)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
@objc func getWebViewMetrics(_ call: CAPPluginCall) {
|
|
190
|
+
DispatchQueue.main.async { [weak self] in
|
|
191
|
+
call.resolve(self?.webViewMetrics() ?? ["radius": 0])
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
@objc func configure(_ call: CAPPluginCall) {
|
|
196
|
+
DispatchQueue.main.async { [weak self] in
|
|
197
|
+
guard let self else { call.resolve(["supported": false]); return }
|
|
198
|
+
self.configurationGeneration += 1
|
|
199
|
+
let generation = self.configurationGeneration
|
|
200
|
+
let configure = { [self] in
|
|
201
|
+
guard generation == self.configurationGeneration else {
|
|
202
|
+
call.reject("Configuration superseded"); return
|
|
203
|
+
}
|
|
204
|
+
// A new JS context starts revision numbering again (live reload / navigation).
|
|
205
|
+
self.restoreTopEdge?()
|
|
206
|
+
self.restoreTopEdge = nil
|
|
207
|
+
self.removeControls()
|
|
208
|
+
self.revision = 0
|
|
209
|
+
if #available(iOS 26.0, *) {
|
|
210
|
+
self.bridge?.webView?.layoutIfNeeded()
|
|
211
|
+
// Ionic already paints the header edge; a second native effect can
|
|
212
|
+
// add a dark scrim when the OS and Web themes differ.
|
|
213
|
+
if call.getBool("verticalBarsOnly") != true, let effect = self.bridge?.webView?.scrollView.topEdgeEffect {
|
|
214
|
+
let hidden = effect.isHidden
|
|
215
|
+
effect.isHidden = true
|
|
216
|
+
self.restoreTopEdge = { [weak effect] in effect?.isHidden = hidden }
|
|
217
|
+
}
|
|
218
|
+
call.resolve(["supported": true, "extensions": NativeUI.names])
|
|
219
|
+
}
|
|
220
|
+
else { call.resolve(["supported": false]) }
|
|
221
|
+
}
|
|
222
|
+
// A replacement JS context cannot close its predecessor's child window.
|
|
223
|
+
// Finish dismissing it while its rail still owns the SwiftUI presentation.
|
|
224
|
+
if let overlays = self.overlays {
|
|
225
|
+
overlays.stop {
|
|
226
|
+
if self.overlays === overlays { self.overlays = nil }
|
|
227
|
+
configure()
|
|
228
|
+
}
|
|
229
|
+
} else { configure() }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
@objc func getVerticalBarsGeometry(_ call: CAPPluginCall) {
|
|
234
|
+
DispatchQueue.main.async { [weak self] in
|
|
235
|
+
guard let self, let webView = self.bridge?.webView, let rail = self.verticalBars,
|
|
236
|
+
let width = call.getDouble("viewportWidth"), width.isFinite, width > 0,
|
|
237
|
+
webView.bounds.width > 0 else { call.resolve(["items": [:]]); return }
|
|
238
|
+
let scale = width / webView.bounds.width
|
|
239
|
+
let ids = Set(call.getArray("ids", String.self) ?? [])
|
|
240
|
+
let measured = NativeUIVerticalBarsGeometry.measure(in: rail.view, relativeTo: webView, ids: ids).merging(rail.measuredFrames(relativeTo: webView).filter { ids.contains($0.key) }) { _, swiftUI in swiftUI }
|
|
241
|
+
let items = measured.mapValues { frame in
|
|
242
|
+
["x": frame.minX * scale, "y": frame.minY * scale,
|
|
243
|
+
"width": frame.width * scale, "height": frame.height * scale]
|
|
244
|
+
}
|
|
245
|
+
call.resolve(["revision": self.revision, "items": items])
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
@objc func completeExtensionAction(_ call: CAPPluginCall) {
|
|
250
|
+
DispatchQueue.main.async { [weak self] in
|
|
251
|
+
guard let request = call.getString("requestId"), let accepted = call.getBool("accepted") else {
|
|
252
|
+
call.reject("Invalid extension acknowledgement"); return
|
|
253
|
+
}
|
|
254
|
+
self?.extensionActions.finish(request, error: accepted ? nil : NativeUIError.rejected)
|
|
255
|
+
call.resolve()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
@MainActor private func extensionContent(_ node: NativeUIControl, viewportWidth: CGFloat, scale: CGFloat) throws -> AnyView {
|
|
260
|
+
guard let data = node.extensionContent else { throw NativeUIError.unavailable }
|
|
261
|
+
let session = extensionSessions[node.id] ?? UUID()
|
|
262
|
+
extensionSessions[node.id] = session
|
|
263
|
+
let actions = NativeUIActions(availableNames: Set(data.actions.keys)) { [weak self] name in
|
|
264
|
+
guard let self, self.extensionNodes[node.id] == node, self.extensionSessions[node.id] == session, let target = data.actions[name] else {
|
|
265
|
+
throw NativeUIError.unavailable
|
|
266
|
+
}
|
|
267
|
+
try await self.extensionActions.perform(owner: node.id) { request in
|
|
268
|
+
self.sequence += 1
|
|
269
|
+
self.notifyListeners("activate", data: ["id": target, "revision": self.revision,
|
|
270
|
+
"sequence": self.sequence, "requestId": request])
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
let context = NativeUIContext(node: node, viewportWidth: viewportWidth, pointsPerCSSPixel: scale)
|
|
274
|
+
return AnyView(try NativeUI.content(data, actions: actions, context: context)
|
|
275
|
+
.environment(\.layoutDirection, node.rtl ? .rightToLeft : .leftToRight)
|
|
276
|
+
.environment(\.colorScheme, node.dark ? .dark : .light)
|
|
277
|
+
.id(node.id))
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
@objc func clear(_ call: CAPPluginCall) {
|
|
281
|
+
DispatchQueue.main.async { [weak self] in
|
|
282
|
+
guard let self else { call.resolve(); return }
|
|
283
|
+
let next = call.getInt("revision") ?? 0
|
|
284
|
+
if next >= self.revision {
|
|
285
|
+
self.revision = next
|
|
286
|
+
self.restoreTopEdge?()
|
|
287
|
+
self.restoreTopEdge = nil
|
|
288
|
+
self.removeControls()
|
|
289
|
+
}
|
|
290
|
+
call.resolve()
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private func removeControl(_ id: String, duration: TimeInterval = 0) {
|
|
295
|
+
extensionActions.cancel(owner: id)
|
|
296
|
+
extensionNodes.removeValue(forKey: id)
|
|
297
|
+
extensionSessions.removeValue(forKey: id)
|
|
298
|
+
let extensionController = extensionControllers.removeValue(forKey: id)
|
|
299
|
+
searchControllers.removeValue(forKey: id)?.detach()
|
|
300
|
+
let control = controls.removeValue(forKey: id)
|
|
301
|
+
if let extensionController { extensionController.detach(duration: duration) }
|
|
302
|
+
else if let control { NativeUICrossfade.retire(control, duration: duration) }
|
|
303
|
+
fingerprints.removeValue(forKey: id)
|
|
304
|
+
pendingTabSelections.removeValue(forKey: id)
|
|
305
|
+
pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
private func removeControls(duration: TimeInterval = 0) {
|
|
309
|
+
extensionActions.cancel()
|
|
310
|
+
extensionNodes.removeAll()
|
|
311
|
+
extensionSessions.removeAll()
|
|
312
|
+
Array(controls.keys).forEach { removeControl($0, duration: duration) }
|
|
313
|
+
host?.removeFromSuperview()
|
|
314
|
+
host = nil
|
|
315
|
+
verticalBars?.detach(duration: duration)
|
|
316
|
+
verticalBars = nil
|
|
317
|
+
rendering.clear()
|
|
318
|
+
pendingTabSelections.removeAll()
|
|
319
|
+
pendingTabExpiryWorks.values.forEach { $0.cancel() }
|
|
320
|
+
pendingTabExpiryWorks.removeAll()
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private func syncTabBar(_ tabBar: UITabBar, id: String, node: NativeUIControl) {
|
|
324
|
+
var pending = pendingTabSelections[id]
|
|
325
|
+
NativeUITabBar.update(tabBar, node: node, rendering: rendering, pendingSelection: &pending)
|
|
326
|
+
if let pending {
|
|
327
|
+
pendingTabSelections[id] = pending
|
|
328
|
+
} else {
|
|
329
|
+
pendingTabSelections.removeValue(forKey: id)
|
|
330
|
+
pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private func schedulePendingTabExpiry(_ id: String, until: CFAbsoluteTime) {
|
|
335
|
+
pendingTabExpiryWorks[id]?.cancel()
|
|
336
|
+
let delay = max(0, until - CFAbsoluteTimeGetCurrent()) + 0.02
|
|
337
|
+
let work = DispatchWorkItem { [weak self] in
|
|
338
|
+
guard let self else { return }
|
|
339
|
+
self.pendingTabExpiryWorks.removeValue(forKey: id)
|
|
340
|
+
guard let pending = self.pendingTabSelections[id],
|
|
341
|
+
CFAbsoluteTimeGetCurrent() >= pending.until,
|
|
342
|
+
let tabBar = self.controls[id] as? UITabBar,
|
|
343
|
+
let node = self.fingerprints[id] else { return }
|
|
344
|
+
self.syncTabBar(tabBar, id: id, node: node)
|
|
345
|
+
}
|
|
346
|
+
pendingTabExpiryWorks[id] = work
|
|
347
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: work)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
@objc func update(_ call: CAPPluginCall) {
|
|
351
|
+
DispatchQueue.main.async { [weak self] in
|
|
352
|
+
guard let self else { call.reject("Plugin released"); return }
|
|
353
|
+
guard #available(iOS 26.0, *) else { call.reject("Requires iOS 26"); return }
|
|
354
|
+
let next = call.getInt("revision") ?? 0
|
|
355
|
+
guard next > self.revision else { call.resolve(["revision": self.revision]); return }
|
|
356
|
+
guard let webView = self.bridge?.webView, let parent = webView.superview else {
|
|
357
|
+
call.reject("WebView unavailable"); return
|
|
358
|
+
}
|
|
359
|
+
// Decode and validate the entire batch before changing visible controls.
|
|
360
|
+
guard let snapshot = try? call.decode(NativeUISnapshot.self), snapshot.isValid else {
|
|
361
|
+
call.reject("Invalid control snapshot"); return
|
|
362
|
+
}
|
|
363
|
+
self.notifyWebViewMetricsChange()
|
|
364
|
+
let duration = NativeUICrossfade.duration(snapshot.transitionDuration)
|
|
365
|
+
let existing = Set(self.controls.keys)
|
|
366
|
+
let verticalBars = snapshot.controls.filter { $0.placement == .verticalBars }
|
|
367
|
+
let accessories = snapshot.controls.filter { $0.extensionContent?.tabBarId != nil }
|
|
368
|
+
let snapshots = snapshot.controls.filter { $0.placement != .verticalBars && $0.extensionContent?.tabBarId == nil }
|
|
369
|
+
let extensionIDs = Set(snapshot.controls.filter { $0.kind == .extension }.map(\.id))
|
|
370
|
+
for id in Array(self.extensionNodes.keys) where !extensionIDs.contains(id) {
|
|
371
|
+
self.extensionActions.cancel(owner: id)
|
|
372
|
+
self.extensionNodes.removeValue(forKey: id)
|
|
373
|
+
self.extensionSessions.removeValue(forKey: id)
|
|
374
|
+
}
|
|
375
|
+
let verticalSearchActive = verticalBars.contains { $0.search.map { $0.available && $0.active } ?? false }
|
|
376
|
+
let width = snapshot.viewportWidth
|
|
377
|
+
self.revision = next
|
|
378
|
+
if snapshots.isEmpty && verticalBars.isEmpty && accessories.isEmpty {
|
|
379
|
+
self.removeControls(duration: duration)
|
|
380
|
+
call.resolve(["revision": next]); return
|
|
381
|
+
}
|
|
382
|
+
let scale = webView.bounds.width / width
|
|
383
|
+
let retained = Set(snapshots.map(\.id))
|
|
384
|
+
for id in Array(self.controls.keys) where !retained.contains(id) {
|
|
385
|
+
self.removeControl(id, duration: duration)
|
|
386
|
+
}
|
|
387
|
+
var rejectedControls: [String] = []
|
|
388
|
+
var fabs: [(NativeUIFab, NativeUIControl)] = []
|
|
389
|
+
var searches: [(NativeUISearchControlling, NativeUIControl, CGRect, CGRect, UIView?, Bool)] = []
|
|
390
|
+
var rejectedSearches: [String] = []
|
|
391
|
+
if verticalBars.isEmpty || (self.keyboardVisible && !verticalSearchActive) {
|
|
392
|
+
self.verticalBars?.detach(duration: duration)
|
|
393
|
+
self.verticalBars = nil
|
|
394
|
+
if self.keyboardVisible { rejectedControls.append(contentsOf: verticalBars.map(\.id)) }
|
|
395
|
+
} else if let owner = self.bridge?.viewController {
|
|
396
|
+
let entering = self.verticalBars == nil
|
|
397
|
+
let rail = self.verticalBars ?? NativeUIVerticalBarsController(
|
|
398
|
+
activate: { [weak self] id in self?.activate(id) },
|
|
399
|
+
changed: { [weak self] id, phase, value, composing, valueVersion in
|
|
400
|
+
self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
|
|
401
|
+
})
|
|
402
|
+
self.verticalBars = rail
|
|
403
|
+
rail.attach(to: owner, in: owner.view)
|
|
404
|
+
if let frame = snapshot.verticalBarFrame {
|
|
405
|
+
rail.view.frame = webView.convert(frame.rect.applying(CGAffineTransform(scaleX: scale, y: scale)), to: owner.view)
|
|
406
|
+
} else {
|
|
407
|
+
rail.view.frame = owner.view.bounds
|
|
408
|
+
}
|
|
409
|
+
// The shell owns the entry crossfade; later native updates keep SwiftUI motion.
|
|
410
|
+
rail.apply(verticalBars, rendering: self.rendering, edge: snapshot.verticalBarEdge ?? "right", animated: !entering)
|
|
411
|
+
rail.view.isHidden = false
|
|
412
|
+
if entering { NativeUICrossfade.enter(rail.view, duration: duration) }
|
|
413
|
+
} else {
|
|
414
|
+
rejectedControls.append(contentsOf: verticalBars.map(\.id))
|
|
415
|
+
}
|
|
416
|
+
// A system tab host has one accessory slot. Ambiguous owners stay on the Web.
|
|
417
|
+
var accessoryContent: AnyView?
|
|
418
|
+
for node in accessories {
|
|
419
|
+
guard #available(iOS 26.1, *), accessories.count == 1, !self.keyboardVisible, self.verticalBars != nil,
|
|
420
|
+
verticalBars.filter({ $0.kind == .tabBar }).map(\.id) == [node.extensionContent!.tabBarId!],
|
|
421
|
+
let content = try? self.extensionContent(node, viewportWidth: width, scale: scale) else {
|
|
422
|
+
rejectedControls.append(node.id)
|
|
423
|
+
self.extensionNodes.removeValue(forKey: node.id)
|
|
424
|
+
self.extensionSessions.removeValue(forKey: node.id)
|
|
425
|
+
self.extensionActions.cancel(owner: node.id)
|
|
426
|
+
continue
|
|
427
|
+
}
|
|
428
|
+
self.extensionNodes[node.id] = node
|
|
429
|
+
accessoryContent = content
|
|
430
|
+
}
|
|
431
|
+
self.verticalBars?.setAccessory(accessoryContent)
|
|
432
|
+
if snapshots.isEmpty {
|
|
433
|
+
self.host?.removeFromSuperview()
|
|
434
|
+
self.host = nil
|
|
435
|
+
call.resolve(["revision": next, "rejectedControls": rejectedControls])
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
let host = self.host ?? NativeUIHost()
|
|
439
|
+
self.host = host
|
|
440
|
+
host.frame = parent.bounds
|
|
441
|
+
host.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
|
442
|
+
host.backgroundColor = .clear
|
|
443
|
+
host.isAccessibilityElement = false
|
|
444
|
+
UIView.performWithoutAnimation {
|
|
445
|
+
if host.superview !== parent { parent.addSubview(host) }
|
|
446
|
+
for node in snapshots {
|
|
447
|
+
let id = node.id
|
|
448
|
+
if node.kind != .extension, self.extensionControllers[id] != nil { self.removeControl(id) }
|
|
449
|
+
let previous = self.controls[id]
|
|
450
|
+
let previousBounds = previous?.bounds
|
|
451
|
+
let previousCenter = previous?.center
|
|
452
|
+
let reject = {
|
|
453
|
+
// Keep the old cover until JS paints Web and omits this ID.
|
|
454
|
+
if let previous, self.controls[id] === previous {
|
|
455
|
+
previous.bounds = previousBounds!
|
|
456
|
+
previous.center = previousCenter!
|
|
457
|
+
previous.layoutIfNeeded()
|
|
458
|
+
} else { self.removeControl(id) }
|
|
459
|
+
rejectedControls.append(id)
|
|
460
|
+
}
|
|
461
|
+
// Only native search owns its keyboard; other controls return to Web.
|
|
462
|
+
if self.keyboardVisible && self.searchControllers[id]?.ownsKeyboard != true {
|
|
463
|
+
reject(); continue
|
|
464
|
+
}
|
|
465
|
+
let local = node.frame.rect
|
|
466
|
+
let bounds = webView.convert(CGRect(x: local.minX * scale, y: local.minY * scale,
|
|
467
|
+
width: local.width * scale, height: local.height * scale), to: parent)
|
|
468
|
+
if node.kind == .extension {
|
|
469
|
+
guard let owner = self.bridge?.viewController, let content = try? self.extensionContent(node, viewportWidth: width, scale: scale) else {
|
|
470
|
+
self.extensionNodes.removeValue(forKey: id)
|
|
471
|
+
self.extensionSessions.removeValue(forKey: id)
|
|
472
|
+
self.extensionActions.cancel(owner: id)
|
|
473
|
+
reject(); continue
|
|
474
|
+
}
|
|
475
|
+
if let controller = self.extensionControllers[id] {
|
|
476
|
+
controller.update(content)
|
|
477
|
+
} else {
|
|
478
|
+
if let previous = self.controls.removeValue(forKey: id) { previous.removeFromSuperview() }
|
|
479
|
+
self.searchControllers.removeValue(forKey: id)?.detach()
|
|
480
|
+
let controller = NativeUIExtensionController(content: content, owner: owner)
|
|
481
|
+
self.extensionControllers[id] = controller
|
|
482
|
+
self.controls[id] = controller.view
|
|
483
|
+
}
|
|
484
|
+
self.extensionNodes[id] = node
|
|
485
|
+
self.controls[id]?.frame = parent.convert(bounds, to: owner.view)
|
|
486
|
+
self.controls[id]?.isHidden = false
|
|
487
|
+
self.controls[id]?.overrideUserInterfaceStyle = node.dark ? .dark : .light
|
|
488
|
+
continue
|
|
489
|
+
}
|
|
490
|
+
if let search = node.search {
|
|
491
|
+
guard let owner = self.bridge?.viewController else { rejectedSearches.append(id); continue }
|
|
492
|
+
let controller: NativeUISearchControlling
|
|
493
|
+
let previousCover = self.controls[id]
|
|
494
|
+
let replacing = self.searchControllers[id]
|
|
495
|
+
if let existing = replacing { controller = existing }
|
|
496
|
+
else {
|
|
497
|
+
// Keep the ordinary UITabBar cover until the search controller applies.
|
|
498
|
+
controller = NativeUISearchController()
|
|
499
|
+
controller.activate = { [weak self] id in self?.activate(id) }
|
|
500
|
+
controller.changed = { [weak self] id, phase, value, composing, valueVersion in
|
|
501
|
+
self?.searchChanged(id, phase: phase, value: value, composing: composing, valueVersion: valueVersion) ?? 0
|
|
502
|
+
}
|
|
503
|
+
controller.attach(to: owner, in: owner.view)
|
|
504
|
+
self.searchControllers[id] = controller
|
|
505
|
+
self.controls[id] = controller.surface
|
|
506
|
+
}
|
|
507
|
+
let trigger = search.trigger.frame.rect
|
|
508
|
+
let searchBarFrame = webView.convert(CGRect(x: webView.bounds.minX + local.minX * scale,
|
|
509
|
+
y: webView.bounds.minY + local.minY * scale, width: local.width * scale, height: local.height * scale), to: owner.view)
|
|
510
|
+
let triggerFrame = webView.convert(CGRect(x: webView.bounds.minX + trigger.minX * scale,
|
|
511
|
+
y: webView.bounds.minY + trigger.minY * scale, width: trigger.width * scale, height: trigger.height * scale), to: owner.view)
|
|
512
|
+
searches.append((controller, node, searchBarFrame, triggerFrame, previousCover, replacing == nil))
|
|
513
|
+
continue
|
|
514
|
+
} else if self.searchControllers[id] != nil {
|
|
515
|
+
self.removeControl(id)
|
|
516
|
+
}
|
|
517
|
+
if self.fingerprints[id] != node {
|
|
518
|
+
if node.kind == NativeUIFab.kind {
|
|
519
|
+
let fab = (self.controls[id] as? NativeUIFab) ?? NativeUIFab()
|
|
520
|
+
if fab.superview == nil { host.addSubview(fab) }
|
|
521
|
+
self.controls[id] = fab
|
|
522
|
+
fabs.append((fab, node))
|
|
523
|
+
} else if let segment = self.controls[id] as? NativeUISegment, node.kind == NativeUISegment.kind {
|
|
524
|
+
segment.update(node, scale: scale, rendering: self.rendering)
|
|
525
|
+
} else if let tabBar = self.controls[id] as? UITabBar, node.kind == NativeUITabBar.kind {
|
|
526
|
+
self.syncTabBar(tabBar, id: id, node: node)
|
|
527
|
+
} else {
|
|
528
|
+
guard let control = NativeUIComponents.make(node, scale: scale, rendering: self.rendering, tabDelegate: self,
|
|
529
|
+
activate: { [weak self] id in self?.activate(id) }) else { reject(); continue }
|
|
530
|
+
self.controls.removeValue(forKey: id)?.removeFromSuperview()
|
|
531
|
+
self.controls[id] = control
|
|
532
|
+
host.addSubview(control)
|
|
533
|
+
}
|
|
534
|
+
self.fingerprints[id] = node
|
|
535
|
+
} else if let tabBar = self.controls[id] as? UITabBar, self.pendingTabSelections[id] != nil {
|
|
536
|
+
// Resolve an in-flight native tap even when other fingerprint fields are unchanged.
|
|
537
|
+
self.syncTabBar(tabBar, id: id, node: node)
|
|
538
|
+
}
|
|
539
|
+
guard let control = self.controls[id] else { reject(); continue }
|
|
540
|
+
if let tabBar = control as? UITabBar {
|
|
541
|
+
guard NativeUITabBar.fit(tabBar, node: node, bounds: bounds) else { reject(); continue }
|
|
542
|
+
} else if control.frame != bounds {
|
|
543
|
+
control.frame = bounds
|
|
544
|
+
}
|
|
545
|
+
control.overrideUserInterfaceStyle = node.dark ? .dark : .light
|
|
546
|
+
}
|
|
547
|
+
host.isHidden = false
|
|
548
|
+
host.layoutIfNeeded()
|
|
549
|
+
}
|
|
550
|
+
// FABs update in place. Only icon content animates; the glass surface
|
|
551
|
+
// and Ionic's staggered child visibility keep their own identities.
|
|
552
|
+
for (fab, node) in fabs {
|
|
553
|
+
fab.apply(node, scale: scale, rendering: self.rendering, activate: { [weak self] id in self?.activate(id) })
|
|
554
|
+
}
|
|
555
|
+
for (controller, node, frame, triggerFrame, previousCover, created) in searches {
|
|
556
|
+
let id = node.id
|
|
557
|
+
let webFrame = webView.convert(webView.bounds, to: controller.surface.superview)
|
|
558
|
+
if !controller.apply(node, webFrame: webFrame, barFrame: frame, triggerFrame: triggerFrame, rendering: self.rendering) {
|
|
559
|
+
if created {
|
|
560
|
+
self.searchControllers.removeValue(forKey: id)
|
|
561
|
+
controller.detach()
|
|
562
|
+
if let previousCover, previousCover.superview != nil {
|
|
563
|
+
self.controls[id] = previousCover
|
|
564
|
+
} else {
|
|
565
|
+
self.controls.removeValue(forKey: id)?.removeFromSuperview()
|
|
566
|
+
self.fingerprints.removeValue(forKey: id)
|
|
567
|
+
self.pendingTabSelections[id] = nil
|
|
568
|
+
self.pendingTabExpiryWorks.removeValue(forKey: id)?.cancel()
|
|
569
|
+
}
|
|
570
|
+
} else {
|
|
571
|
+
self.removeControl(id)
|
|
572
|
+
}
|
|
573
|
+
rejectedSearches.append(id)
|
|
574
|
+
} else {
|
|
575
|
+
if created, let previousCover, previousCover !== controller.surface {
|
|
576
|
+
previousCover.removeFromSuperview()
|
|
577
|
+
}
|
|
578
|
+
controller.surface.isHidden = self.keyboardVisible && !controller.ownsKeyboard
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
for (id, control) in self.controls where !existing.contains(id) && self.searchControllers[id] == nil {
|
|
582
|
+
NativeUICrossfade.enter(control, duration: duration)
|
|
583
|
+
}
|
|
584
|
+
let complete = { call.resolve(["revision": next, "rejectedSearches": rejectedSearches, "rejectedControls": rejectedControls]) }
|
|
585
|
+
if let coordinator = searches.first?.0.transitionCoordinator,
|
|
586
|
+
coordinator.animate(alongsideTransition: nil, completion: { _ in complete() }) { return }
|
|
587
|
+
complete()
|
|
588
|
+
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
public func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
|
593
|
+
guard item.isEnabled, let itemId = item.accessibilityIdentifier else { return }
|
|
594
|
+
if let controlId = controls.first(where: { $0.value === tabBar })?.key {
|
|
595
|
+
let pending = NativeUITabBar.PendingSelection.start(itemId)
|
|
596
|
+
pendingTabSelections[controlId] = pending
|
|
597
|
+
schedulePendingTabExpiry(controlId, until: pending.until)
|
|
598
|
+
}
|
|
599
|
+
activate(itemId)
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
private func searchChanged(_ id: String, phase: NativeUISearchPhase, value: String,
|
|
603
|
+
composing: Bool, valueVersion: Int) -> Int {
|
|
604
|
+
sequence += 1
|
|
605
|
+
notifyListeners("search", data: ["id": id, "phase": phase.rawValue, "value": value,
|
|
606
|
+
"composing": composing, "valueVersion": valueVersion, "sequence": sequence, "revision": revision])
|
|
607
|
+
return sequence
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private func activate(_ id: String) {
|
|
611
|
+
sequence += 1
|
|
612
|
+
notifyListeners("activate", data: ["id": id, "revision": revision, "sequence": sequence])
|
|
613
|
+
|
|
614
|
+
}
|
|
615
|
+
}
|