@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.
Files changed (230) hide show
  1. package/README.md +14 -0
  2. package/WebViewNativeUIControls.podspec +17 -0
  3. package/dist/controls/activation.d.ts +15 -0
  4. package/dist/controls/activation.js +370 -0
  5. package/dist/controls/components/index.d.ts +16 -0
  6. package/dist/controls/components/index.js +38 -0
  7. package/dist/controls/components/ion-back-button.d.ts +4 -0
  8. package/dist/controls/components/ion-back-button.js +16 -0
  9. package/dist/controls/components/ion-button.d.ts +4 -0
  10. package/dist/controls/components/ion-button.js +26 -0
  11. package/dist/controls/components/ion-buttons.d.ts +5 -0
  12. package/dist/controls/components/ion-buttons.js +34 -0
  13. package/dist/controls/components/ion-fab.d.ts +4 -0
  14. package/dist/controls/components/ion-fab.js +164 -0
  15. package/dist/controls/components/ion-menu-button.d.ts +7 -0
  16. package/dist/controls/components/ion-menu-button.js +41 -0
  17. package/dist/controls/components/ion-segment.d.ts +5 -0
  18. package/dist/controls/components/ion-segment.js +24 -0
  19. package/dist/controls/components/ion-tab-bar.d.ts +5 -0
  20. package/dist/controls/components/ion-tab-bar.js +49 -0
  21. package/dist/controls/components/searchable-tabs.d.ts +20 -0
  22. package/dist/controls/components/searchable-tabs.js +525 -0
  23. package/dist/controls/definitions.d.ts +360 -0
  24. package/dist/controls/definitions.js +1 -0
  25. package/dist/controls/extensions.d.ts +44 -0
  26. package/dist/controls/extensions.js +164 -0
  27. package/dist/controls/foldable/fold-state.d.ts +4 -0
  28. package/dist/controls/foldable/fold-state.js +8 -0
  29. package/dist/controls/foldable/types.d.ts +9 -0
  30. package/dist/controls/foldable/types.js +1 -0
  31. package/dist/controls/html.d.ts +8 -0
  32. package/dist/controls/html.js +260 -0
  33. package/dist/controls/index.d.ts +7 -0
  34. package/dist/controls/index.js +4 -0
  35. package/dist/controls/ionic.d.ts +1 -0
  36. package/dist/controls/ionic.js +1 -0
  37. package/dist/controls/legacy-layout.d.ts +7 -0
  38. package/dist/controls/legacy-layout.js +23 -0
  39. package/dist/controls/overlays/content.d.ts +2 -0
  40. package/dist/controls/overlays/content.js +55 -0
  41. package/dist/controls/overlays/controller.d.ts +7 -0
  42. package/dist/controls/overlays/controller.js +369 -0
  43. package/dist/controls/overlays/covered-overlay.d.ts +4 -0
  44. package/dist/controls/overlays/covered-overlay.js +9 -0
  45. package/dist/controls/overlays/dialog-animation.d.ts +7 -0
  46. package/dist/controls/overlays/dialog-animation.js +49 -0
  47. package/dist/controls/overlays/dismiss-animation.d.ts +2 -0
  48. package/dist/controls/overlays/dismiss-animation.js +14 -0
  49. package/dist/controls/overlays/focus.d.ts +2 -0
  50. package/dist/controls/overlays/focus.js +67 -0
  51. package/dist/controls/overlays/popover.d.ts +10 -0
  52. package/dist/controls/overlays/popover.js +117 -0
  53. package/dist/controls/overlays/styles.d.ts +9 -0
  54. package/dist/controls/overlays/styles.js +194 -0
  55. package/dist/controls/prehide.d.ts +6 -0
  56. package/dist/controls/prehide.js +280 -0
  57. package/dist/controls/runtime.d.ts +5 -0
  58. package/dist/controls/runtime.js +1685 -0
  59. package/dist/controls/shared/bridge.d.ts +4 -0
  60. package/dist/controls/shared/bridge.js +15 -0
  61. package/dist/controls/shared/candidate.d.ts +23 -0
  62. package/dist/controls/shared/candidate.js +128 -0
  63. package/dist/controls/shared/clipping.d.ts +1 -0
  64. package/dist/controls/shared/clipping.js +1 -0
  65. package/dist/controls/shared/connection.d.ts +21 -0
  66. package/dist/controls/shared/connection.js +112 -0
  67. package/dist/controls/shared/control-options.d.ts +4 -0
  68. package/dist/controls/shared/control-options.js +14 -0
  69. package/dist/controls/shared/covered-snapshot.d.ts +35 -0
  70. package/dist/controls/shared/covered-snapshot.js +41 -0
  71. package/dist/controls/shared/covered-source.d.ts +22 -0
  72. package/dist/controls/shared/covered-source.js +72 -0
  73. package/dist/controls/shared/crossfade.d.ts +11 -0
  74. package/dist/controls/shared/crossfade.js +71 -0
  75. package/dist/controls/shared/diagnostics.d.ts +2 -0
  76. package/dist/controls/shared/diagnostics.js +16 -0
  77. package/dist/controls/shared/dom.d.ts +45 -0
  78. package/dist/controls/shared/dom.js +258 -0
  79. package/dist/controls/shared/events.d.ts +12 -0
  80. package/dist/controls/shared/events.js +13 -0
  81. package/dist/controls/shared/horizontal-navigation.d.ts +36 -0
  82. package/dist/controls/shared/horizontal-navigation.js +90 -0
  83. package/dist/controls/shared/icons.d.ts +5 -0
  84. package/dist/controls/shared/icons.js +66 -0
  85. package/dist/controls/shared/modal-animation.d.ts +21 -0
  86. package/dist/controls/shared/modal-animation.js +97 -0
  87. package/dist/controls/shared/modal.d.ts +26 -0
  88. package/dist/controls/shared/modal.js +261 -0
  89. package/dist/controls/shared/projection-id.d.ts +2 -0
  90. package/dist/controls/shared/projection-id.js +2 -0
  91. package/dist/controls/shared/scroll-placement.d.ts +1 -0
  92. package/dist/controls/shared/scroll-placement.js +1 -0
  93. package/dist/controls/shared/tab-bar-width.d.ts +7 -0
  94. package/dist/controls/shared/tab-bar-width.js +72 -0
  95. package/dist/controls/shared/toolbar-options.d.ts +10 -0
  96. package/dist/controls/shared/toolbar-options.js +38 -0
  97. package/dist/controls/shared/transition-identity.d.ts +3 -0
  98. package/dist/controls/shared/transition-identity.js +64 -0
  99. package/dist/controls/shared/vertical-bars-geometry.d.ts +8 -0
  100. package/dist/controls/shared/vertical-bars-geometry.js +82 -0
  101. package/dist/controls/shared/vertical-navigation.d.ts +5 -0
  102. package/dist/controls/shared/vertical-navigation.js +39 -0
  103. package/dist/controls/startup.d.ts +7 -0
  104. package/dist/controls/startup.js +72 -0
  105. package/dist/controls/transport.d.ts +10 -0
  106. package/dist/controls/transport.js +1 -0
  107. package/dist/controls/vertical-bars-web-search.d.ts +8 -0
  108. package/dist/controls/vertical-bars-web-search.js +83 -0
  109. package/dist/controls/vertical-bars-web.d.ts +2 -0
  110. package/dist/controls/vertical-bars-web.js +609 -0
  111. package/dist/index.d.ts +1 -0
  112. package/dist/index.js +1 -0
  113. package/dist/integration/index.d.ts +36 -0
  114. package/dist/integration/index.js +25 -0
  115. package/dist/native/session.js +14 -8
  116. package/dist/projection/pause.d.ts +10 -0
  117. package/dist/projection/pause.js +58 -0
  118. package/dist/projection/styles.d.ts +1 -1
  119. package/dist/projection/styles.js +0 -1
  120. package/dist/projection/surfaces.js +2 -1
  121. package/dist/projection/web.js +39 -5
  122. package/dist/types.d.ts +5 -1
  123. package/docs/README.md +1 -0
  124. package/docs/configuration/README.md +3 -1
  125. package/docs/configuration/ionic-overlays.md +36 -2
  126. package/docs/configuration/pause-resume.md +51 -0
  127. package/docs/controls.md +39 -0
  128. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButton.swift +98 -0
  129. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIButtons.swift +24 -0
  130. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIComponents.swift +19 -0
  131. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIFab.swift +80 -0
  132. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISearchController.swift +449 -0
  133. package/ios/Sources/WebViewNativeUIControls/Components/NativeUISegment.swift +91 -0
  134. package/ios/Sources/WebViewNativeUIControls/Components/NativeUITabBar.swift +213 -0
  135. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIToolbarTransition.swift +186 -0
  136. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalBars.swift +696 -0
  137. package/ios/Sources/WebViewNativeUIControls/Components/NativeUIVerticalSearch.swift +117 -0
  138. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUI.swift +180 -0
  139. package/ios/Sources/WebViewNativeUIControls/Extensions/NativeUIRendering.swift +38 -0
  140. package/ios/Sources/WebViewNativeUIControls/NativeUIController.swift +801 -0
  141. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayController.swift +131 -0
  142. package/ios/Sources/WebViewNativeUIControls/Overlays/NativeUIOverlayHost.swift +85 -0
  143. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIControlRendering.swift +21 -0
  144. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICoveredSnapshots.swift +14 -0
  145. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUICrossfade.swift +35 -0
  146. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIHost.swift +8 -0
  147. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIScrollTracker.swift +2 -0
  148. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUISnapshot.swift +206 -0
  149. package/ios/Sources/WebViewNativeUIControls/Shared/NativeUIVerticalBarsGeometry.swift +93 -0
  150. package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +2 -2
  151. package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +1 -1
  152. package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -4
  153. package/package.json +30 -5
  154. package/src/controls/activation.ts +427 -0
  155. package/src/controls/components/index.ts +45 -0
  156. package/src/controls/components/ion-back-button.ts +22 -0
  157. package/src/controls/components/ion-button.ts +28 -0
  158. package/src/controls/components/ion-buttons.ts +37 -0
  159. package/src/controls/components/ion-fab.ts +151 -0
  160. package/src/controls/components/ion-menu-button.ts +37 -0
  161. package/src/controls/components/ion-segment.ts +27 -0
  162. package/src/controls/components/ion-tab-bar.ts +55 -0
  163. package/src/controls/components/searchable-tabs.ts +534 -0
  164. package/src/controls/definitions.ts +338 -0
  165. package/src/controls/extensions.ts +198 -0
  166. package/src/controls/foldable/fold-state.ts +10 -0
  167. package/src/controls/foldable/types.ts +4 -0
  168. package/src/controls/html.ts +239 -0
  169. package/src/controls/index.ts +7 -0
  170. package/src/controls/ionic.ts +1 -0
  171. package/src/controls/legacy-layout.ts +21 -0
  172. package/src/controls/overlays/content.ts +51 -0
  173. package/src/controls/overlays/controller.ts +394 -0
  174. package/src/controls/overlays/covered-overlay.ts +11 -0
  175. package/src/controls/overlays/dialog-animation.ts +53 -0
  176. package/src/controls/overlays/dismiss-animation.ts +15 -0
  177. package/src/controls/overlays/focus.ts +64 -0
  178. package/src/controls/overlays/popover.ts +116 -0
  179. package/src/controls/overlays/styles.ts +195 -0
  180. package/src/controls/prehide.ts +285 -0
  181. package/src/controls/runtime.ts +1731 -0
  182. package/src/controls/shared/bridge.ts +18 -0
  183. package/src/controls/shared/candidate.ts +163 -0
  184. package/src/controls/shared/clipping.ts +1 -0
  185. package/src/controls/shared/connection.ts +105 -0
  186. package/src/controls/shared/control-options.ts +20 -0
  187. package/src/controls/shared/covered-snapshot.ts +52 -0
  188. package/src/controls/shared/covered-source.ts +78 -0
  189. package/src/controls/shared/crossfade.ts +70 -0
  190. package/src/controls/shared/diagnostics.ts +13 -0
  191. package/src/controls/shared/dom.ts +310 -0
  192. package/src/controls/shared/events.ts +14 -0
  193. package/src/controls/shared/horizontal-navigation.ts +106 -0
  194. package/src/controls/shared/icons.ts +64 -0
  195. package/src/controls/shared/modal-animation.ts +109 -0
  196. package/src/controls/shared/modal.ts +286 -0
  197. package/src/controls/shared/projection-id.ts +2 -0
  198. package/src/controls/shared/scroll-placement.ts +1 -0
  199. package/src/controls/shared/tab-bar-width.ts +66 -0
  200. package/src/controls/shared/toolbar-options.ts +51 -0
  201. package/src/controls/shared/transition-identity.ts +67 -0
  202. package/src/controls/shared/vertical-bars-geometry.ts +92 -0
  203. package/src/controls/shared/vertical-navigation.ts +34 -0
  204. package/src/controls/startup.ts +74 -0
  205. package/src/controls/transport.ts +11 -0
  206. package/src/controls/vertical-bars-web-search.ts +86 -0
  207. package/src/controls/vertical-bars-web.ts +674 -0
  208. package/src/dom/geometry.ts +24 -0
  209. package/src/dom/overlays.ts +196 -0
  210. package/src/dom/scroll-bindings.ts +66 -0
  211. package/src/dom/scroll-placement.ts +92 -0
  212. package/src/dom/visibility.ts +29 -0
  213. package/src/index.ts +19 -0
  214. package/src/integration/index.ts +64 -0
  215. package/src/ionic.ts +34 -0
  216. package/src/native/bootstrap.ts +132 -0
  217. package/src/native/bridge.ts +18 -0
  218. package/src/native/resource-url.ts +11 -0
  219. package/src/native/session.ts +708 -0
  220. package/src/native/updates.ts +103 -0
  221. package/src/native/values.ts +29 -0
  222. package/src/projection/events.ts +58 -0
  223. package/src/projection/pause.ts +58 -0
  224. package/src/projection/placement.ts +34 -0
  225. package/src/projection/scope.ts +13 -0
  226. package/src/projection/styles.ts +26 -0
  227. package/src/projection/surface.ts +88 -0
  228. package/src/projection/surfaces.ts +119 -0
  229. package/src/projection/web.ts +1013 -0
  230. 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,8 @@
1
+ import UIKit
2
+
3
+ final class NativeUIHost: UIView {
4
+ override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
5
+ let hit = super.hitTest(point, with: event)
6
+ return hit === self ? nil : hit
7
+ }
8
+ }
@@ -0,0 +1,2 @@
1
+ import WebViewNativeUIEngine
2
+ typealias NativeUIScrollPlacement = WebViewNativeUIEngine.NativeUIScrollPlacement
@@ -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([:])