@webview-native-ui/engine 1.0.1 → 1.0.2
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 +14 -0
- package/WebViewNativeUIControls.podspec +17 -0
- package/dist/controls/activation.d.ts +15 -0
- package/dist/controls/activation.js +370 -0
- package/dist/controls/components/index.d.ts +16 -0
- package/dist/controls/components/index.js +38 -0
- package/dist/controls/components/ion-back-button.d.ts +4 -0
- package/dist/controls/components/ion-back-button.js +16 -0
- package/dist/controls/components/ion-button.d.ts +4 -0
- package/dist/controls/components/ion-button.js +26 -0
- package/dist/controls/components/ion-buttons.d.ts +5 -0
- package/dist/controls/components/ion-buttons.js +34 -0
- package/dist/controls/components/ion-fab.d.ts +4 -0
- package/dist/controls/components/ion-fab.js +164 -0
- package/dist/controls/components/ion-menu-button.d.ts +7 -0
- package/dist/controls/components/ion-menu-button.js +41 -0
- package/dist/controls/components/ion-segment.d.ts +5 -0
- package/dist/controls/components/ion-segment.js +24 -0
- package/dist/controls/components/ion-tab-bar.d.ts +5 -0
- package/dist/controls/components/ion-tab-bar.js +49 -0
- package/dist/controls/components/searchable-tabs.d.ts +20 -0
- package/dist/controls/components/searchable-tabs.js +525 -0
- package/dist/controls/definitions.d.ts +360 -0
- package/dist/controls/definitions.js +1 -0
- package/dist/controls/extensions.d.ts +44 -0
- package/dist/controls/extensions.js +164 -0
- package/dist/controls/foldable/fold-state.d.ts +4 -0
- package/dist/controls/foldable/fold-state.js +8 -0
- package/dist/controls/foldable/types.d.ts +9 -0
- package/dist/controls/foldable/types.js +1 -0
- package/dist/controls/html.d.ts +8 -0
- package/dist/controls/html.js +260 -0
- package/dist/controls/index.d.ts +7 -0
- package/dist/controls/index.js +4 -0
- package/dist/controls/ionic.d.ts +1 -0
- package/dist/controls/ionic.js +1 -0
- package/dist/controls/legacy-layout.d.ts +7 -0
- package/dist/controls/legacy-layout.js +23 -0
- package/dist/controls/overlays/content.d.ts +2 -0
- package/dist/controls/overlays/content.js +55 -0
- package/dist/controls/overlays/controller.d.ts +7 -0
- package/dist/controls/overlays/controller.js +369 -0
- package/dist/controls/overlays/covered-overlay.d.ts +4 -0
- package/dist/controls/overlays/covered-overlay.js +9 -0
- package/dist/controls/overlays/dialog-animation.d.ts +7 -0
- package/dist/controls/overlays/dialog-animation.js +49 -0
- package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
- package/dist/controls/overlays/dismiss-animation.js +14 -0
- package/dist/controls/overlays/focus.d.ts +2 -0
- package/dist/controls/overlays/focus.js +67 -0
- package/dist/controls/overlays/popover.d.ts +10 -0
- package/dist/controls/overlays/popover.js +117 -0
- package/dist/controls/overlays/styles.d.ts +9 -0
- package/dist/controls/overlays/styles.js +194 -0
- package/dist/controls/prehide.d.ts +6 -0
- package/dist/controls/prehide.js +280 -0
- package/dist/controls/runtime.d.ts +5 -0
- package/dist/controls/runtime.js +1685 -0
- package/dist/controls/shared/bridge.d.ts +4 -0
- package/dist/controls/shared/bridge.js +15 -0
- package/dist/controls/shared/candidate.d.ts +23 -0
- package/dist/controls/shared/candidate.js +128 -0
- package/dist/controls/shared/clipping.d.ts +1 -0
- package/dist/controls/shared/clipping.js +1 -0
- package/dist/controls/shared/connection.d.ts +21 -0
- package/dist/controls/shared/connection.js +112 -0
- package/dist/controls/shared/control-options.d.ts +4 -0
- package/dist/controls/shared/control-options.js +14 -0
- package/dist/controls/shared/covered-snapshot.d.ts +35 -0
- package/dist/controls/shared/covered-snapshot.js +41 -0
- package/dist/controls/shared/covered-source.d.ts +22 -0
- package/dist/controls/shared/covered-source.js +72 -0
- package/dist/controls/shared/crossfade.d.ts +11 -0
- package/dist/controls/shared/crossfade.js +71 -0
- package/dist/controls/shared/diagnostics.d.ts +2 -0
- package/dist/controls/shared/diagnostics.js +16 -0
- package/dist/controls/shared/dom.d.ts +45 -0
- package/dist/controls/shared/dom.js +258 -0
- package/dist/controls/shared/events.d.ts +12 -0
- package/dist/controls/shared/events.js +13 -0
- package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
- package/dist/controls/shared/horizontal-navigation.js +90 -0
- package/dist/controls/shared/icons.d.ts +5 -0
- package/dist/controls/shared/icons.js +66 -0
- package/dist/controls/shared/modal-animation.d.ts +21 -0
- package/dist/controls/shared/modal-animation.js +97 -0
- package/dist/controls/shared/modal.d.ts +26 -0
- package/dist/controls/shared/modal.js +261 -0
- package/dist/controls/shared/projection-id.d.ts +2 -0
- package/dist/controls/shared/projection-id.js +2 -0
- package/dist/controls/shared/scroll-placement.d.ts +1 -0
- package/dist/controls/shared/scroll-placement.js +1 -0
- package/dist/controls/shared/tab-bar-width.d.ts +7 -0
- package/dist/controls/shared/tab-bar-width.js +72 -0
- package/dist/controls/shared/toolbar-options.d.ts +10 -0
- package/dist/controls/shared/toolbar-options.js +38 -0
- package/dist/controls/shared/transition-identity.d.ts +3 -0
- package/dist/controls/shared/transition-identity.js +64 -0
- package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
- package/dist/controls/shared/vertical-bars-geometry.js +82 -0
- package/dist/controls/shared/vertical-navigation.d.ts +5 -0
- package/dist/controls/shared/vertical-navigation.js +39 -0
- package/dist/controls/startup.d.ts +7 -0
- package/dist/controls/startup.js +72 -0
- package/dist/controls/transport.d.ts +10 -0
- package/dist/controls/transport.js +1 -0
- package/dist/controls/vertical-bars-web-search.d.ts +8 -0
- package/dist/controls/vertical-bars-web-search.js +83 -0
- package/dist/controls/vertical-bars-web.d.ts +2 -0
- package/dist/controls/vertical-bars-web.js +609 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/integration/index.d.ts +36 -0
- package/dist/integration/index.js +25 -0
- package/dist/native/session.js +14 -8
- package/dist/projection/pause.d.ts +10 -0
- package/dist/projection/pause.js +58 -0
- package/dist/projection/styles.d.ts +1 -1
- package/dist/projection/styles.js +0 -1
- package/dist/projection/surfaces.js +2 -1
- package/dist/projection/web.js +39 -5
- package/dist/types.d.ts +5 -1
- package/docs/README.md +1 -0
- package/docs/configuration/README.md +3 -1
- package/docs/configuration/ionic-overlays.md +36 -2
- package/docs/configuration/pause-resume.md +51 -0
- package/docs/controls.md +39 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
- package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
- package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
- package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
- package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
- package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
- package/package.json +30 -5
- package/src/controls/activation.ts +427 -0
- package/src/controls/components/index.ts +45 -0
- package/src/controls/components/ion-back-button.ts +22 -0
- package/src/controls/components/ion-button.ts +28 -0
- package/src/controls/components/ion-buttons.ts +37 -0
- package/src/controls/components/ion-fab.ts +151 -0
- package/src/controls/components/ion-menu-button.ts +37 -0
- package/src/controls/components/ion-segment.ts +27 -0
- package/src/controls/components/ion-tab-bar.ts +55 -0
- package/src/controls/components/searchable-tabs.ts +534 -0
- package/src/controls/definitions.ts +338 -0
- package/src/controls/extensions.ts +198 -0
- package/src/controls/foldable/fold-state.ts +10 -0
- package/src/controls/foldable/types.ts +4 -0
- package/src/controls/html.ts +239 -0
- package/src/controls/index.ts +7 -0
- package/src/controls/ionic.ts +1 -0
- package/src/controls/legacy-layout.ts +21 -0
- package/src/controls/overlays/content.ts +51 -0
- package/src/controls/overlays/controller.ts +394 -0
- package/src/controls/overlays/covered-overlay.ts +11 -0
- package/src/controls/overlays/dialog-animation.ts +53 -0
- package/src/controls/overlays/dismiss-animation.ts +15 -0
- package/src/controls/overlays/focus.ts +64 -0
- package/src/controls/overlays/popover.ts +116 -0
- package/src/controls/overlays/styles.ts +195 -0
- package/src/controls/prehide.ts +285 -0
- package/src/controls/runtime.ts +1731 -0
- package/src/controls/shared/bridge.ts +18 -0
- package/src/controls/shared/candidate.ts +163 -0
- package/src/controls/shared/clipping.ts +1 -0
- package/src/controls/shared/connection.ts +105 -0
- package/src/controls/shared/control-options.ts +20 -0
- package/src/controls/shared/covered-snapshot.ts +52 -0
- package/src/controls/shared/covered-source.ts +78 -0
- package/src/controls/shared/crossfade.ts +70 -0
- package/src/controls/shared/diagnostics.ts +13 -0
- package/src/controls/shared/dom.ts +310 -0
- package/src/controls/shared/events.ts +14 -0
- package/src/controls/shared/horizontal-navigation.ts +106 -0
- package/src/controls/shared/icons.ts +64 -0
- package/src/controls/shared/modal-animation.ts +109 -0
- package/src/controls/shared/modal.ts +286 -0
- package/src/controls/shared/projection-id.ts +2 -0
- package/src/controls/shared/scroll-placement.ts +1 -0
- package/src/controls/shared/tab-bar-width.ts +66 -0
- package/src/controls/shared/toolbar-options.ts +51 -0
- package/src/controls/shared/transition-identity.ts +67 -0
- package/src/controls/shared/vertical-bars-geometry.ts +92 -0
- package/src/controls/shared/vertical-navigation.ts +34 -0
- package/src/controls/startup.ts +74 -0
- package/src/controls/transport.ts +11 -0
- package/src/controls/vertical-bars-web-search.ts +86 -0
- package/src/controls/vertical-bars-web.ts +674 -0
- package/src/dom/geometry.ts +24 -0
- package/src/dom/overlays.ts +196 -0
- package/src/dom/scroll-bindings.ts +66 -0
- package/src/dom/scroll-placement.ts +92 -0
- package/src/dom/visibility.ts +29 -0
- package/src/index.ts +19 -0
- package/src/integration/index.ts +64 -0
- package/src/ionic.ts +34 -0
- package/src/native/bootstrap.ts +132 -0
- package/src/native/bridge.ts +18 -0
- package/src/native/resource-url.ts +11 -0
- package/src/native/session.ts +708 -0
- package/src/native/updates.ts +103 -0
- package/src/native/values.ts +29 -0
- package/src/projection/events.ts +58 -0
- package/src/projection/pause.ts +58 -0
- package/src/projection/placement.ts +34 -0
- package/src/projection/scope.ts +13 -0
- package/src/projection/styles.ts +26 -0
- package/src/projection/surface.ts +88 -0
- package/src/projection/surfaces.ts +119 -0
- package/src/projection/web.ts +1013 -0
- package/src/types.ts +58 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// Intercepts only a prepared blank window. Other requests keep Capacitor's behavior.
|
|
5
|
+
final class NativeUIOverlayController: NSObject, WKUIDelegate {
|
|
6
|
+
private weak var source: WKWebView?
|
|
7
|
+
private weak var owner: UIViewController?
|
|
8
|
+
private let original: WKUIDelegate?
|
|
9
|
+
private let automaticallyOpensWindows: Bool
|
|
10
|
+
private var prepared: String?
|
|
11
|
+
private let event: (String, String) -> Void
|
|
12
|
+
private var id: String?
|
|
13
|
+
private var host: NativeUIOverlayHost?
|
|
14
|
+
|
|
15
|
+
init(source: WKWebView, owner: UIViewController, event: @escaping (String, String) -> Void) {
|
|
16
|
+
self.source = source
|
|
17
|
+
self.owner = owner
|
|
18
|
+
self.event = event
|
|
19
|
+
original = source.uiDelegate
|
|
20
|
+
automaticallyOpensWindows = source.configuration.preferences.javaScriptCanOpenWindowsAutomatically
|
|
21
|
+
super.init()
|
|
22
|
+
source.uiDelegate = self
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// swiftlint:disable:next implicitly_unwrapped_optional
|
|
26
|
+
override func responds(to selector: Selector!) -> Bool {
|
|
27
|
+
super.responds(to: selector) || original?.responds(to: selector) == true
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// swiftlint:disable:next implicitly_unwrapped_optional
|
|
31
|
+
override func forwardingTarget(for selector: Selector!) -> Any? {
|
|
32
|
+
original?.responds(to: selector) == true ? original : super.forwardingTarget(for: selector)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
func prepare(_ id: String) -> Bool {
|
|
36
|
+
guard prepared == nil, host == nil else { return false }
|
|
37
|
+
prepared = id
|
|
38
|
+
source?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
39
|
+
return true
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration,
|
|
43
|
+
for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
44
|
+
// WebKit percent-encodes '#' inside about: URLs; compare the decoded form.
|
|
45
|
+
guard webView === source, navigationAction.sourceFrame.isMainFrame,
|
|
46
|
+
let prepared, navigationAction.targetFrame == nil,
|
|
47
|
+
navigationAction.request.url?.absoluteString.removingPercentEncoding == "about:blank#\(prepared)" else {
|
|
48
|
+
return original?.webView?(webView, createWebViewWith: configuration,
|
|
49
|
+
for: navigationAction, windowFeatures: windowFeatures)
|
|
50
|
+
}
|
|
51
|
+
self.prepared = nil
|
|
52
|
+
source?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = automaticallyOpensWindows
|
|
53
|
+
id = prepared
|
|
54
|
+
let host = NativeUIOverlayHost(configuration: configuration, options: nil) { [weak self] action in
|
|
55
|
+
self?.event(prepared, action)
|
|
56
|
+
}
|
|
57
|
+
host.webView.uiDelegate = self
|
|
58
|
+
host.webView.frame = source?.bounds ?? .zero
|
|
59
|
+
self.host = host
|
|
60
|
+
return host.webView
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Read ownership without layout or geometry work. Keep the terminal state
|
|
64
|
+
// until Web closes this connection, so a lost event can be reconciled.
|
|
65
|
+
var connectionState: [String: String]? {
|
|
66
|
+
guard let id, let host else { return nil }
|
|
67
|
+
let state = host.disappeared ? "dismissed" :
|
|
68
|
+
(host.anchoredVisible && host.viewIfLoaded?.window != nil ? "presented" : "missing")
|
|
69
|
+
return ["id": id, "state": state]
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func present(_ id: String, options: [String: Any]?, anchored: (String, NativeUIOverlayHost, @escaping (Bool) -> Void) -> Bool,
|
|
73
|
+
completion: @escaping (Bool) -> Void) {
|
|
74
|
+
guard self.id == id, let host, let owner else { completion(false); return }
|
|
75
|
+
if host.anchoredVisible && host.viewIfLoaded?.window != nil { completion(true); return }
|
|
76
|
+
guard owner.presentedViewController == nil else { completion(false); return }
|
|
77
|
+
host.apply(options)
|
|
78
|
+
var settled = false
|
|
79
|
+
var timeout: DispatchWorkItem?
|
|
80
|
+
let complete: (Bool) -> Void = { presented in
|
|
81
|
+
guard !settled else { return }
|
|
82
|
+
settled = true
|
|
83
|
+
timeout?.cancel()
|
|
84
|
+
timeout = nil
|
|
85
|
+
completion(presented)
|
|
86
|
+
}
|
|
87
|
+
timeout = DispatchWorkItem { [weak host] in
|
|
88
|
+
host?.cancelAnchored?()
|
|
89
|
+
complete(false)
|
|
90
|
+
}
|
|
91
|
+
// swiftlint:disable:next force_unwrapping
|
|
92
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + NativeUIOverlayHost.completionTimeout, execute: timeout!)
|
|
93
|
+
if host.options?["kind"] as? String == "popover",
|
|
94
|
+
let anchor = host.options?["anchorId"] as? String,
|
|
95
|
+
anchored(anchor, host, complete) { return }
|
|
96
|
+
complete(false)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Keep the WebView alive until JavaScript has restored the adopted nodes and listeners.
|
|
100
|
+
func dismiss(_ id: String, animated: Bool, completion: @escaping () -> Void) {
|
|
101
|
+
guard self.id == id, let host else { completion(); return }
|
|
102
|
+
host.dismissPopover(animated: animated, completion: completion)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
func close(_ id: String, completion: @escaping () -> Void) {
|
|
106
|
+
if prepared == id {
|
|
107
|
+
prepared = nil
|
|
108
|
+
source?.configuration.preferences.javaScriptCanOpenWindowsAutomatically = automaticallyOpensWindows
|
|
109
|
+
}
|
|
110
|
+
guard self.id == id, let host else { completion(); return }
|
|
111
|
+
let finish = { [self] in
|
|
112
|
+
host.webView.uiDelegate = nil
|
|
113
|
+
self.host = nil
|
|
114
|
+
self.id = nil
|
|
115
|
+
completion()
|
|
116
|
+
}
|
|
117
|
+
host.dismissPopover(animated: false, completion: finish)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func stop(completion: @escaping () -> Void) {
|
|
121
|
+
let finish = { [self] in
|
|
122
|
+
prepared = nil
|
|
123
|
+
if let source, source.uiDelegate === self {
|
|
124
|
+
source.uiDelegate = original
|
|
125
|
+
source.configuration.preferences.javaScriptCanOpenWindowsAutomatically = automaticallyOpensWindows
|
|
126
|
+
}
|
|
127
|
+
completion()
|
|
128
|
+
}
|
|
129
|
+
if let id { close(id, completion: finish) } else { finish() }
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
/// Hosts only the live content of a Vertical Bars popover.
|
|
5
|
+
final class NativeUIOverlayHost: UIViewController {
|
|
6
|
+
let webView: WKWebView
|
|
7
|
+
private(set) var options: [String: Any]?
|
|
8
|
+
let event: (String) -> Void
|
|
9
|
+
// Settle native presentation before the JS bridge's five-second deadline.
|
|
10
|
+
static let completionTimeout: TimeInterval = 4
|
|
11
|
+
var dismissAnchored: (() -> Void)?
|
|
12
|
+
var cancelAnchored: (() -> Void)?
|
|
13
|
+
var anchoredVisible = false
|
|
14
|
+
var disappeared = false
|
|
15
|
+
private var dismissalCompletions: [() -> Void]?
|
|
16
|
+
private var dismissalTimeout: DispatchWorkItem?
|
|
17
|
+
|
|
18
|
+
func presentationDidDisappear() {
|
|
19
|
+
// SwiftUI may disappear before (or without) updating isPresented.
|
|
20
|
+
// Notify through this retained host, not the model's already-cleared host.
|
|
21
|
+
// Web-initiated dismissal already owns Ionic's lifecycle; do not echo it.
|
|
22
|
+
let nativeDismissal = anchoredVisible && dismissalCompletions == nil
|
|
23
|
+
if nativeDismissal { disappeared = true }
|
|
24
|
+
completeDismissal()
|
|
25
|
+
if nativeDismissal { event("dismiss") }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func completeDismissal() {
|
|
29
|
+
dismissalTimeout?.cancel()
|
|
30
|
+
dismissalTimeout = nil
|
|
31
|
+
anchoredVisible = false
|
|
32
|
+
let completions = dismissalCompletions ?? []
|
|
33
|
+
dismissalCompletions = nil
|
|
34
|
+
completions.forEach { $0() }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Dismiss, close and stop may overlap while SwiftUI is still disappearing.
|
|
38
|
+
// Start dismissal once and deliver its completion to every waiting caller.
|
|
39
|
+
func dismissPopover(animated: Bool, completion: @escaping () -> Void) {
|
|
40
|
+
if dismissalCompletions != nil {
|
|
41
|
+
dismissalCompletions?.append(completion)
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
dismissalCompletions = [completion]
|
|
45
|
+
let finish = { [self] in completeDismissal() }
|
|
46
|
+
let timeout = DispatchWorkItem { [weak self] in
|
|
47
|
+
guard let self, self.dismissalCompletions != nil else { return }
|
|
48
|
+
// Remove the owning rail if SwiftUI never reports disappearance.
|
|
49
|
+
// Its detach path settles waiters before any replacement can attach.
|
|
50
|
+
self.cancelAnchored?()
|
|
51
|
+
self.completeDismissal()
|
|
52
|
+
}
|
|
53
|
+
dismissalTimeout = timeout
|
|
54
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.completionTimeout, execute: timeout)
|
|
55
|
+
if let dismissAnchored {
|
|
56
|
+
if anchoredVisible { dismissAnchored() } else { dismissAnchored(); finish() }
|
|
57
|
+
} else if presentingViewController != nil {
|
|
58
|
+
dismiss(animated: animated, completion: finish)
|
|
59
|
+
} else { finish() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
init(configuration: WKWebViewConfiguration, options: [String: Any]?, event: @escaping (String) -> Void) {
|
|
63
|
+
webView = WKWebView(frame: .zero, configuration: configuration)
|
|
64
|
+
self.event = event
|
|
65
|
+
super.init(nibName: nil, bundle: nil)
|
|
66
|
+
apply(options)
|
|
67
|
+
}
|
|
68
|
+
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
|
|
69
|
+
|
|
70
|
+
func apply(_ options: [String: Any]?) {
|
|
71
|
+
guard let options else { return }
|
|
72
|
+
self.options = options
|
|
73
|
+
preferredContentSize = CGSize(width: options["width"] as? Double ?? 280, height: options["height"] as? Double ?? 200)
|
|
74
|
+
overrideUserInterfaceStyle = options["dark"] as? Bool == true ? .dark : .light
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
override func loadView() {
|
|
78
|
+
webView.isOpaque = false
|
|
79
|
+
webView.backgroundColor = .clear
|
|
80
|
+
webView.scrollView.backgroundColor = .clear
|
|
81
|
+
webView.scrollView.contentInsetAdjustmentBehavior = .never
|
|
82
|
+
view = webView
|
|
83
|
+
view.accessibilityViewIsModal = true
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
final class NativeUIControlRendering {
|
|
4
|
+
private var images: [String: UIImage] = [:]
|
|
5
|
+
func clear() { images.removeAll() }
|
|
6
|
+
|
|
7
|
+
func image(_ item: NativeUIItemContent) -> UIImage? {
|
|
8
|
+
guard let data = item.icon, let width = item.iconWidth else { return nil }
|
|
9
|
+
let template = item.iconTemplate == true
|
|
10
|
+
let key = "\(width):" + (template ? "template:" : "original:") + data
|
|
11
|
+
if let cached = images[key] { return cached }
|
|
12
|
+
guard let result = NativeUIRendering.image(NativeUIImage(data: data, width: width, height: item.iconHeight ?? width, template: template)) else { return nil }
|
|
13
|
+
if images.count >= 128 { images.removeAll() }
|
|
14
|
+
images[key] = result
|
|
15
|
+
return result
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
func color(_ css: String?) -> UIColor {
|
|
19
|
+
NativeUIRendering.color(css)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import WebViewNativeUIEngine
|
|
3
|
+
typealias NativeUICoveredSnapshots = WebViewNativeUIEngine.NativeUICoveredSnapshots
|
|
4
|
+
|
|
5
|
+
/// A public, non-interactive layout anchor for content hosted inside the system tab accessory.
|
|
6
|
+
struct NativeUICoveredSnapshotAnchor: UIViewRepresentable {
|
|
7
|
+
let id: String
|
|
8
|
+
func makeUIView(context: Context) -> UIView {
|
|
9
|
+
let view = UIView()
|
|
10
|
+
view.isUserInteractionEnabled = false
|
|
11
|
+
return view
|
|
12
|
+
}
|
|
13
|
+
func updateUIView(_ view: UIView, context: Context) { view.accessibilityIdentifier = id }
|
|
14
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
@MainActor enum NativeUICrossfade {
|
|
4
|
+
static func duration(_ milliseconds: Double?) -> TimeInterval {
|
|
5
|
+
UIAccessibility.isReduceMotionEnabled ? 0 : (milliseconds ?? 0) / 1000
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
static func enter(_ view: UIView, duration: TimeInterval) {
|
|
9
|
+
guard duration > 0 else { return }
|
|
10
|
+
view.alpha = 0
|
|
11
|
+
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseInOut, .allowUserInteraction]) {
|
|
12
|
+
view.alpha = 1
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static func retire(_ view: UIView, duration: TimeInterval) {
|
|
17
|
+
// The outgoing copy is visual only: it cannot keep accepting actions or
|
|
18
|
+
// VoiceOver focus once the Web source has been restored.
|
|
19
|
+
if duration > 0, let host = view.superview, let parent = host.superview,
|
|
20
|
+
!view.isHidden, !host.isHidden, let copy = view.snapshotView(afterScreenUpdates: false) {
|
|
21
|
+
copy.frame = host.convert(view.frame, to: parent)
|
|
22
|
+
copy.alpha = CGFloat(view.layer.presentation()?.opacity ?? Float(view.alpha))
|
|
23
|
+
copy.isUserInteractionEnabled = false
|
|
24
|
+
copy.isAccessibilityElement = false
|
|
25
|
+
copy.accessibilityElementsHidden = true
|
|
26
|
+
parent.insertSubview(copy, aboveSubview: host)
|
|
27
|
+
UIView.animate(withDuration: duration, delay: 0, options: [.curveEaseInOut]) {
|
|
28
|
+
copy.alpha = 0
|
|
29
|
+
} completion: { _ in
|
|
30
|
+
copy.removeFromSuperview()
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
view.removeFromSuperview()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreGraphics
|
|
3
|
+
|
|
4
|
+
// Standard renderer names are independent of DOM tags and Web frameworks.
|
|
5
|
+
enum NativeUIComponent: String, Decodable {
|
|
6
|
+
case `extension`
|
|
7
|
+
case button, buttons = "button-group"
|
|
8
|
+
case backButton = "back-button", menuButton = "menu-button"
|
|
9
|
+
case tabBar = "tab-bar", segment, fab = "floating-action-button"
|
|
10
|
+
|
|
11
|
+
init(from decoder: Decoder) throws {
|
|
12
|
+
let container = try decoder.singleValueContainer()
|
|
13
|
+
let value = try container.decode(String.self)
|
|
14
|
+
// Accept previous clients during native/Web bundle upgrades.
|
|
15
|
+
let legacy: [String: NativeUIComponent] = [
|
|
16
|
+
"ion-button": .button, "ion-buttons": .buttons,
|
|
17
|
+
"ion-back-button": .backButton, "ion-menu-button": .menuButton,
|
|
18
|
+
"ion-tab-bar": .tabBar, "ion-segment": .segment, "ion-fab": .fab
|
|
19
|
+
]
|
|
20
|
+
guard let kind = NativeUIComponent(rawValue: value) ?? legacy[value] else {
|
|
21
|
+
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Unknown native control kind")
|
|
22
|
+
}
|
|
23
|
+
self = kind
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
struct NativeUIToolbarTransitionSnapshot: Decodable {
|
|
28
|
+
let id: Int
|
|
29
|
+
let sourceIds: [String]
|
|
30
|
+
let destination: [NativeUIControl]
|
|
31
|
+
var isValid: Bool {
|
|
32
|
+
id > 0 && sourceIds.count <= 100 && Set(sourceIds).count == sourceIds.count && destination.count <= 100 &&
|
|
33
|
+
Set(destination.map(\.id)).count == destination.count && destination.allSatisfy {
|
|
34
|
+
$0.isValid && $0.placement == nil && (NativeUIButton.kinds + [.buttons]).contains($0.kind)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
struct NativeUIModalAnimation: Decodable {
|
|
40
|
+
let id: Int
|
|
41
|
+
let startedAt: Double
|
|
42
|
+
let duration: Double
|
|
43
|
+
let fromY: Double
|
|
44
|
+
let opacity: Double
|
|
45
|
+
let easing: [Double]
|
|
46
|
+
var isValid: Bool {
|
|
47
|
+
id > 0 && [startedAt, duration, fromY, opacity].allSatisfy(\.isFinite) &&
|
|
48
|
+
duration > 0 && duration <= 60_000 && opacity >= 0 && opacity <= 1 &&
|
|
49
|
+
easing.count == 4 && easing.allSatisfy(\.isFinite) &&
|
|
50
|
+
(0...1).contains(easing[0]) && (0...1).contains(easing[2])
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
struct NativeUISnapshot: Decodable {
|
|
55
|
+
let revision: Int
|
|
56
|
+
var coveredSnapshotToken: String?
|
|
57
|
+
let transitionDuration: Double?
|
|
58
|
+
let toolbarTransition: NativeUIToolbarTransitionSnapshot?
|
|
59
|
+
let viewportWidth: Double
|
|
60
|
+
let verticalBarFrame: NativeUIFrame?
|
|
61
|
+
let modalAnimation: NativeUIModalAnimation?
|
|
62
|
+
let verticalBarEdge: String?
|
|
63
|
+
let verticalBarNavigation: [String]?
|
|
64
|
+
let verticalBarNavigationPhase: String?
|
|
65
|
+
let controls: [NativeUIControl]
|
|
66
|
+
|
|
67
|
+
var isValid: Bool {
|
|
68
|
+
(toolbarTransition.map { $0.isValid && Set($0.sourceIds).isSubset(of: Set(controls.map(\.id))) } ?? true) &&
|
|
69
|
+
revision >= 0 && (transitionDuration.map { $0.isFinite && $0 >= 0 && $0 <= 500 } ?? true) && viewportWidth.isFinite && viewportWidth > 0 && controls.count <= 100 &&
|
|
70
|
+
(verticalBarNavigationPhase == nil || verticalBarNavigationPhase == "tracking" || verticalBarNavigationPhase == "settling") &&
|
|
71
|
+
(verticalBarNavigation.map { Set($0).count == $0.count && $0.allSatisfy { !$0.isEmpty } } ?? true) &&
|
|
72
|
+
(verticalBarFrame.map { $0.isValid } ?? true) &&
|
|
73
|
+
(modalAnimation.map { $0.isValid && verticalBarFrame != nil } ?? true) &&
|
|
74
|
+
(verticalBarEdge == nil || verticalBarEdge == "left" || verticalBarEdge == "right") &&
|
|
75
|
+
Set(controls.map(\.id)).count == controls.count && controls.allSatisfy(\.isValid)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Frames and content decode from the same flat wire object. Keeping them separate
|
|
80
|
+
// lets FABs compare artwork without treating movement or visibility as new content.
|
|
81
|
+
struct NativeUIFrame: Decodable, Equatable {
|
|
82
|
+
let x: Double
|
|
83
|
+
let y: Double
|
|
84
|
+
let width: Double
|
|
85
|
+
let height: Double
|
|
86
|
+
var rect: CGRect { CGRect(x: x, y: y, width: width, height: height) }
|
|
87
|
+
var isValid: Bool { [x, y, width, height].allSatisfy(\.isFinite) && width > 0 && height > 0 }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
struct NativeUIControl: Decodable, Equatable {
|
|
91
|
+
enum Placement: String, Decodable { case verticalBars = "vertical-bars" }
|
|
92
|
+
enum ToolbarSlot: String, Decodable { case start, end }
|
|
93
|
+
let id: String
|
|
94
|
+
let kind: NativeUIComponent
|
|
95
|
+
let placement: Placement?
|
|
96
|
+
let toolbarSlot: ToolbarSlot?
|
|
97
|
+
let transitionId: String?
|
|
98
|
+
let frame: NativeUIFrame
|
|
99
|
+
let items: [NativeUIItem]
|
|
100
|
+
let dark: Bool
|
|
101
|
+
let rtl: Bool
|
|
102
|
+
let tabTint: String?
|
|
103
|
+
let tabBarAnchor: NativeUITabBar.Anchor?
|
|
104
|
+
let search: NativeUISearch?
|
|
105
|
+
let extensionContent: NativeUIExtensionContent?
|
|
106
|
+
let scroll: NativeUIScrollPlacement?
|
|
107
|
+
|
|
108
|
+
private enum CodingKeys: String, CodingKey {
|
|
109
|
+
case id, kind, placement, toolbarSlot, transitionId, items, dark, rtl, tabBarAnchor, tabTint, search
|
|
110
|
+
case scroll
|
|
111
|
+
case extensionContent = "extension"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
init(from decoder: Decoder) throws {
|
|
115
|
+
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
116
|
+
id = try values.decode(String.self, forKey: .id)
|
|
117
|
+
kind = try values.decode(NativeUIComponent.self, forKey: .kind)
|
|
118
|
+
placement = try values.decodeIfPresent(Placement.self, forKey: .placement)
|
|
119
|
+
toolbarSlot = try values.decodeIfPresent(ToolbarSlot.self, forKey: .toolbarSlot)
|
|
120
|
+
transitionId = try values.decodeIfPresent(String.self, forKey: .transitionId)
|
|
121
|
+
frame = try NativeUIFrame(from: decoder)
|
|
122
|
+
items = try values.decode([NativeUIItem].self, forKey: .items)
|
|
123
|
+
dark = try values.decode(Bool.self, forKey: .dark)
|
|
124
|
+
rtl = try values.decode(Bool.self, forKey: .rtl)
|
|
125
|
+
tabTint = try values.decodeIfPresent(String.self, forKey: .tabTint)
|
|
126
|
+
tabBarAnchor = try values.decodeIfPresent(NativeUITabBar.Anchor.self, forKey: .tabBarAnchor)
|
|
127
|
+
search = try values.decodeIfPresent(NativeUISearch.self, forKey: .search)
|
|
128
|
+
scroll = try values.decodeIfPresent(NativeUIScrollPlacement.self, forKey: .scroll)
|
|
129
|
+
extensionContent = try values.decodeIfPresent(NativeUIExtensionContent.self, forKey: .extensionContent)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
var isValid: Bool {
|
|
133
|
+
if kind == .extension {
|
|
134
|
+
return !id.isEmpty && frame.isValid && items.isEmpty && placement == nil && toolbarSlot == nil &&
|
|
135
|
+
search == nil && tabBarAnchor == nil && extensionContent?.isValid == true &&
|
|
136
|
+
(scroll.map { $0.isValid && extensionContent?.tabBarId == nil } ?? true)
|
|
137
|
+
}
|
|
138
|
+
return scroll == nil && extensionContent == nil && NativeUIComponents.supported.contains(kind) && !id.isEmpty && frame.isValid && !items.isEmpty && items.count <= 30 &&
|
|
139
|
+
(!NativeUIButton.kinds.contains(kind) || items.count == 1) &&
|
|
140
|
+
(toolbarSlot == nil || (placement == .verticalBars && [.button, .buttons, .menuButton].contains(kind))) &&
|
|
141
|
+
Set(items.map(\.id)).count == items.count && items.allSatisfy(\.isValid) &&
|
|
142
|
+
(tabBarAnchor.map { kind == .tabBar && $0.isValid } ?? true) &&
|
|
143
|
+
(search.map { kind == .tabBar && $0.isValid } ?? true)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
struct NativeUIItem: Decodable, Equatable {
|
|
148
|
+
let frame: NativeUIFrame
|
|
149
|
+
let content: NativeUIItemContent
|
|
150
|
+
let visible: Bool?
|
|
151
|
+
var id: String { content.id }
|
|
152
|
+
|
|
153
|
+
private enum CodingKeys: String, CodingKey { case visible }
|
|
154
|
+
|
|
155
|
+
init(from decoder: Decoder) throws {
|
|
156
|
+
frame = try NativeUIFrame(from: decoder)
|
|
157
|
+
content = try NativeUIItemContent(from: decoder)
|
|
158
|
+
visible = try decoder.container(keyedBy: CodingKeys.self).decodeIfPresent(Bool.self, forKey: .visible)
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
var isValid: Bool { frame.isValid && content.isValid }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
struct NativeUIBadge: Decodable, Equatable {
|
|
165
|
+
let value: String
|
|
166
|
+
let color: String
|
|
167
|
+
let textColor: String
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
struct NativeUIItemContent: Decodable, Equatable {
|
|
171
|
+
enum IconPosition: String, Decodable { case leading, trailing, top }
|
|
172
|
+
enum ButtonFill: String, Decodable { case clear, solid, outline }
|
|
173
|
+
let id: String
|
|
174
|
+
let transitionId: String?
|
|
175
|
+
var label: String
|
|
176
|
+
let accessibilityLabel: String
|
|
177
|
+
let disabled: Bool
|
|
178
|
+
let selected: Bool
|
|
179
|
+
let fontSize: Double
|
|
180
|
+
let fontWeight: Double
|
|
181
|
+
let color: String
|
|
182
|
+
let buttonFill: ButtonFill?
|
|
183
|
+
let backgroundColor: String?
|
|
184
|
+
let borderColor: String?
|
|
185
|
+
let borderWidth: CGFloat?
|
|
186
|
+
let badge: NativeUIBadge?
|
|
187
|
+
var icon: String?
|
|
188
|
+
var iconWidth: Double?
|
|
189
|
+
var iconHeight: Double?
|
|
190
|
+
let iconPosition: IconPosition?
|
|
191
|
+
let imagePadding: Double?
|
|
192
|
+
let contentInsetLeading: Double?
|
|
193
|
+
let contentInsetTrailing: Double?
|
|
194
|
+
let iconTemplate: Bool?
|
|
195
|
+
let closeIcon: String?
|
|
196
|
+
let closeIconWidth: Double?
|
|
197
|
+
let closeIconHeight: Double?
|
|
198
|
+
let iconTransition: Double?
|
|
199
|
+
|
|
200
|
+
var isValid: Bool {
|
|
201
|
+
!id.isEmpty && fontSize.isFinite && fontSize >= 0 && fontWeight.isFinite &&
|
|
202
|
+
[iconWidth, iconHeight, closeIconWidth, closeIconHeight].compactMap { $0 }.allSatisfy { $0.isFinite && $0 > 0 } &&
|
|
203
|
+
[imagePadding, contentInsetLeading, contentInsetTrailing].compactMap { $0 }.allSatisfy { $0.isFinite } &&
|
|
204
|
+
(iconTransition.map { $0.isFinite && $0 >= 0 } ?? true)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Reads public accessibility geometry after the system has laid out its rail.
|
|
4
|
+
/// IDs bind measurements to current DOM controls without private class names.
|
|
5
|
+
/// Compact square ancestors approximate the system button surface; this remains
|
|
6
|
+
/// a layout heuristic and invalid measurements fall back to Web CSS.
|
|
7
|
+
enum NativeUIVerticalBarsGeometry {
|
|
8
|
+
static func measure(in root: UIView, relativeTo webView: UIView, ids: Set<String>) -> [String: CGRect] {
|
|
9
|
+
guard let window = root.window, window === webView.window else { return [:] }
|
|
10
|
+
root.layoutIfNeeded()
|
|
11
|
+
var result: [String: CGRect] = [:]
|
|
12
|
+
var visited = Set<ObjectIdentifier>()
|
|
13
|
+
func visit(_ object: NSObject, depth: Int) {
|
|
14
|
+
guard depth < 40, visited.count < 2000, visited.insert(ObjectIdentifier(object)).inserted else { return }
|
|
15
|
+
if let id = (object as? UIView)?.accessibilityIdentifier ?? (object as? UIAccessibilityElement)?.accessibilityIdentifier, ids.contains(id) {
|
|
16
|
+
let screenFrame = object.accessibilityFrame
|
|
17
|
+
let frame = (object as? UIView).map { view -> CGRect in
|
|
18
|
+
var surface = view
|
|
19
|
+
var ancestor = view.superview
|
|
20
|
+
while let current = ancestor, current !== root,
|
|
21
|
+
current.bounds.width < 100, current.bounds.height < 100 {
|
|
22
|
+
if current.bounds.width >= max(surface.bounds.width, 1), current.bounds.height >= max(surface.bounds.height, 1),
|
|
23
|
+
abs(current.bounds.width - current.bounds.height) < 1 { surface = current }
|
|
24
|
+
ancestor = current.superview
|
|
25
|
+
}
|
|
26
|
+
return surface.convert(surface.bounds, to: webView)
|
|
27
|
+
} ?? webView.convert(screenFrame, from: window.screen.coordinateSpace)
|
|
28
|
+
if !frame.isNull, !frame.isInfinite, frame.width > 0, frame.height > 0,
|
|
29
|
+
webView.bounds.intersects(frame), frame.width < webView.bounds.width * 0.75 {
|
|
30
|
+
// UIKit may expose a label and its larger button with the same ID.
|
|
31
|
+
// swiftlint:disable:next force_unwrapping
|
|
32
|
+
if result[id] == nil || frame.width * frame.height > result[id]!.width * result[id]!.height {
|
|
33
|
+
result[id] = frame
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if let view = object as? UIView { view.subviews.forEach { visit($0, depth: depth + 1) } }
|
|
38
|
+
if object === webView { return }
|
|
39
|
+
if let elements = object.accessibilityElements {
|
|
40
|
+
elements.compactMap { $0 as? NSObject }.forEach { visit($0, depth: depth + 1) }
|
|
41
|
+
} else {
|
|
42
|
+
let count = object.accessibilityElementCount()
|
|
43
|
+
if count > 0 && count < 200 {
|
|
44
|
+
for index in 0..<count {
|
|
45
|
+
if let element = object.accessibilityElement(at: index) as? NSObject { visit(element, depth: depth + 1) }
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
visit(root, depth: 0)
|
|
51
|
+
return result
|
|
52
|
+
}
|
|
53
|
+
/// When AX is lazy, bind the visible UIKit controls by the same start/end
|
|
54
|
+
/// ordering used by our toolbar. A count mismatch declines the measurement.
|
|
55
|
+
static func componentFrames(in root: UIView, relativeTo webView: UIView, controls: [NativeUIControl]) -> [String: CGRect] {
|
|
56
|
+
guard controls.allSatisfy({ [.button, .buttons, .backButton, .menuButton, .tabBar].contains($0.kind) }),
|
|
57
|
+
controls.allSatisfy({ $0.search == nil }) else { return [:] }
|
|
58
|
+
let ordered = controls.filter { $0.kind != .tabBar && ($0.toolbarSlot == .start || $0.kind == .backButton) }
|
|
59
|
+
+ controls.filter { $0.kind != .tabBar && $0.toolbarSlot != .start && $0.kind != .backButton }
|
|
60
|
+
+ controls.filter { $0.kind == .tabBar }
|
|
61
|
+
var frames: [CGRect] = []
|
|
62
|
+
func visit(_ view: UIView) {
|
|
63
|
+
guard !view.isHidden, view.alpha > 0.05 else { return }
|
|
64
|
+
if view is UIControl {
|
|
65
|
+
var surface = view
|
|
66
|
+
var ancestor = view.superview
|
|
67
|
+
while let current = ancestor, current !== root, current.bounds.width < 100, current.bounds.height < 100 {
|
|
68
|
+
if current.bounds.width >= surface.bounds.width, current.bounds.height >= surface.bounds.height,
|
|
69
|
+
abs(current.bounds.width - current.bounds.height) < 1 { surface = current }
|
|
70
|
+
ancestor = current.superview
|
|
71
|
+
}
|
|
72
|
+
let frame = surface.convert(surface.bounds, to: webView)
|
|
73
|
+
if frame.width > 0, frame.height > 0, frame.width < 100, frame.height < 100,
|
|
74
|
+
webView.bounds.contains(frame), !frames.contains(frame) { frames.append(frame) }
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
view.subviews.forEach(visit)
|
|
78
|
+
}
|
|
79
|
+
visit(root)
|
|
80
|
+
guard frames.count == ordered.reduce(0, { $0 + $1.items.count }) else { return [:] }
|
|
81
|
+
frames.sort { $0.midY < $1.midY }
|
|
82
|
+
guard let first = frames.first, frames.allSatisfy({ abs($0.midX - first.midX) < 2 }),
|
|
83
|
+
zip(frames, frames.dropFirst()).allSatisfy({ $0.midY < $1.midY }) else { return [:] }
|
|
84
|
+
var result: [String: CGRect] = [:]
|
|
85
|
+
var offset = 0
|
|
86
|
+
for control in ordered {
|
|
87
|
+
result[control.id] = frames[offset..<(offset + control.items.count)].reduce(CGRect.null) { $0.union($1) }
|
|
88
|
+
offset += control.items.count
|
|
89
|
+
}
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
}
|
|
@@ -60,7 +60,7 @@ public final class NativeUICoveredSnapshots {
|
|
|
60
60
|
complete(["shown": restore(in: webView, token: token, frames: destination)])
|
|
61
61
|
case "painted", "restored":
|
|
62
62
|
retireCopy(token, entry: request["action"] as? String == "painted")
|
|
63
|
-
complete([:])
|
|
63
|
+
complete(request["action"] as? String == "painted" ? ["shown": captures[token] != nil] : [:])
|
|
64
64
|
case "release": release(token); complete([:])
|
|
65
65
|
case "reset": reset(); complete([:])
|
|
66
66
|
default: complete([:])
|
|
@@ -257,7 +257,7 @@ public final class NativeUICoveredSnapshots {
|
|
|
257
257
|
// A lost cleanup message must never leave an obsolete cover on live UI.
|
|
258
258
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
259
259
|
guard self?.restoration?.views.first === views.first else { return }
|
|
260
|
-
self?.clearRestoration()
|
|
260
|
+
if entry { self?.release(token) } else { self?.clearRestoration() }
|
|
261
261
|
}
|
|
262
262
|
return true
|
|
263
263
|
}
|
|
@@ -320,7 +320,7 @@ public final class NativeUIProjection: NSObject, WKScriptMessageHandler {
|
|
|
320
320
|
}
|
|
321
321
|
}
|
|
322
322
|
complete([:])
|
|
323
|
-
case "painted": snapshots.retireCopy(token); complete([:])
|
|
323
|
+
case "painted": snapshots.retireCopy(token); complete(["shown": !snapshots.identifiers(for: token).isEmpty])
|
|
324
324
|
case "restored": snapshots.retireCopy(token, entry: false); complete([:])
|
|
325
325
|
case "release": snapshots.release(token); captured.removeValue(forKey: token); anchors.removeValue(forKey: token); complete([:])
|
|
326
326
|
case "reset": snapshots.reset(); captured.removeAll(); anchors.removeAll(); complete([:])
|