@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,118 @@
|
|
|
1
|
+
import Combine
|
|
2
|
+
import SwiftUI
|
|
3
|
+
import UIKit
|
|
4
|
+
|
|
5
|
+
@available(iOS 26.0, *)
|
|
6
|
+
final class NativeUIVerticalSearchModel: ObservableObject {
|
|
7
|
+
@Published private(set) var configuration: NativeUISearch
|
|
8
|
+
@Published var text: String
|
|
9
|
+
@Published var presented: Bool
|
|
10
|
+
@Published var focused: Bool
|
|
11
|
+
private weak var railView: UIView?
|
|
12
|
+
private let activate: (String) -> Void
|
|
13
|
+
private let changed: (String, NativeUISearchPhase, String, Bool, Int) -> Int
|
|
14
|
+
private let inputDelegate = NativeUISearchInputDelegate()
|
|
15
|
+
private var editingSequence = 0
|
|
16
|
+
private var valueVersion: Int
|
|
17
|
+
|
|
18
|
+
init(_ configuration: NativeUISearch, in railView: UIView,
|
|
19
|
+
activate: @escaping (String) -> Void,
|
|
20
|
+
changed: @escaping (String, NativeUISearchPhase, String, Bool, Int) -> Int) {
|
|
21
|
+
self.configuration = configuration
|
|
22
|
+
self.railView = railView
|
|
23
|
+
self.activate = activate
|
|
24
|
+
self.changed = changed
|
|
25
|
+
text = configuration.value
|
|
26
|
+
presented = configuration.available && configuration.active
|
|
27
|
+
focused = configuration.focused
|
|
28
|
+
valueVersion = configuration.valueVersion
|
|
29
|
+
inputDelegate.clear = { [weak self] in self?.emit(.clear) }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var ownsKeyboardChrome: Bool { presented && (focused || textField?.isFirstResponder == true) }
|
|
33
|
+
|
|
34
|
+
private var textField: UISearchTextField? {
|
|
35
|
+
func find(_ view: UIView) -> UISearchTextField? {
|
|
36
|
+
guard !view.isHidden, view.alpha > 0.01 else { return nil }
|
|
37
|
+
if let field = view as? UISearchTextField { return field }
|
|
38
|
+
for child in view.subviews {
|
|
39
|
+
if let field = find(child) { return field }
|
|
40
|
+
}
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
return railView.flatMap(find)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func apply(_ next: NativeUISearch) {
|
|
47
|
+
let previous = configuration
|
|
48
|
+
configuration = next
|
|
49
|
+
if next.valueVersion != valueVersion || next.editSequence >= editingSequence {
|
|
50
|
+
valueVersion = next.valueVersion
|
|
51
|
+
text = next.value
|
|
52
|
+
}
|
|
53
|
+
presented = next.available && next.active
|
|
54
|
+
// The first Web acknowledgement still carries the old focus state.
|
|
55
|
+
if !next.active { focused = false }
|
|
56
|
+
else if previous.focused != next.focused { focused = next.focused }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func input(_ value: String) {
|
|
60
|
+
text = value
|
|
61
|
+
emit(.input)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func present(_ value: Bool) {
|
|
65
|
+
presented = value
|
|
66
|
+
if value != configuration.active {
|
|
67
|
+
activate(value ? configuration.trigger.id : configuration.closeId)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func focus(_ value: Bool) {
|
|
72
|
+
focused = value
|
|
73
|
+
emit(value ? .focus : .blur)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func commit() { emit(.commit) }
|
|
77
|
+
|
|
78
|
+
func configureField() {
|
|
79
|
+
guard let field = textField else { return }
|
|
80
|
+
field.accessibilityIdentifier = configuration.id
|
|
81
|
+
field.accessibilityLabel = configuration.field.accessibilityLabel
|
|
82
|
+
field.isEnabled = !configuration.disabled
|
|
83
|
+
field.autocapitalizationType = .none
|
|
84
|
+
field.autocorrectionType = .no
|
|
85
|
+
field.spellCheckingType = .no
|
|
86
|
+
field.clearButtonMode = .always
|
|
87
|
+
if field.delegate !== inputDelegate {
|
|
88
|
+
inputDelegate.original = field.delegate
|
|
89
|
+
field.delegate = inputDelegate
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private func emit(_ phase: NativeUISearchPhase) {
|
|
94
|
+
editingSequence = changed(configuration.id, phase, text,
|
|
95
|
+
textField?.markedTextRange != nil, valueVersion)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@available(iOS 26.0, *)
|
|
100
|
+
struct NativeUIVerticalSearchModifier: ViewModifier {
|
|
101
|
+
@ObservedObject var model: NativeUIVerticalSearchModel
|
|
102
|
+
@FocusState private var focused: Bool
|
|
103
|
+
|
|
104
|
+
@ViewBuilder func body(content: Content) -> some View {
|
|
105
|
+
if model.configuration.available {
|
|
106
|
+
content
|
|
107
|
+
.searchable(text: Binding(get: { model.text }, set: model.input),
|
|
108
|
+
isPresented: Binding(get: { model.presented }, set: model.present),
|
|
109
|
+
placement: .toolbar, prompt: Text(model.configuration.placeholder))
|
|
110
|
+
.searchToolbarBehavior(.minimize)
|
|
111
|
+
.searchPresentationToolbarBehavior(.avoidHidingContent)
|
|
112
|
+
.searchFocused($focused)
|
|
113
|
+
.onSubmit(of: .search, model.commit)
|
|
114
|
+
.onChange(of: focused) { _, value in model.focus(value) }
|
|
115
|
+
.onChange(of: model.focused) { _, value in focused = value }
|
|
116
|
+
} else { content }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
import UIKit
|
|
3
|
+
|
|
4
|
+
/// Experimental API: registration interfaces and behavior may change in future releases.
|
|
5
|
+
/// App-local SwiftUI renderers. Register once before enabling the shell from JavaScript.
|
|
6
|
+
@MainActor public enum NativeUI {
|
|
7
|
+
private static var renderers: [String: (String, NativeUIActions, NativeUIContext) throws -> AnyView] = [:]
|
|
8
|
+
|
|
9
|
+
/// The name must match defineNativeUI in the application's TypeScript source.
|
|
10
|
+
public static func register<Props: Decodable, Content: View>(
|
|
11
|
+
_ name: String, @ViewBuilder content: @escaping (Props, NativeUIActions) -> Content
|
|
12
|
+
) throws {
|
|
13
|
+
try register(name) { (props: Props, actions: NativeUIActions, _: NativeUIContext) in
|
|
14
|
+
content(props, actions)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Receives source geometry and presentation state on every accepted snapshot.
|
|
19
|
+
public static func register<Props: Decodable, Content: View>(
|
|
20
|
+
_ name: String, @ViewBuilder content: @escaping (Props, NativeUIActions, NativeUIContext) -> Content
|
|
21
|
+
) throws {
|
|
22
|
+
guard !name.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty, renderers[name] == nil else {
|
|
23
|
+
throw NativeUIError.duplicateRegistration
|
|
24
|
+
}
|
|
25
|
+
renderers[name] = { json, actions, context in
|
|
26
|
+
let props = try JSONDecoder().decode(Props.self, from: Data(json.utf8))
|
|
27
|
+
return AnyView(content(props, actions, context))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
static var names: [String] { renderers.keys.sorted() }
|
|
32
|
+
|
|
33
|
+
static func content(_ data: NativeUIExtensionContent, actions: NativeUIActions, context: NativeUIContext) throws -> AnyView {
|
|
34
|
+
guard let renderer = renderers[data.name] else { throw NativeUIError.unavailable }
|
|
35
|
+
return try renderer(data.props, actions, context)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Read-only metadata for a projected Web source. Shell still owns its placement.
|
|
40
|
+
public struct NativeUIContext {
|
|
41
|
+
public enum Placement: String { case element, tabAccessory = "tab-accessory" }
|
|
42
|
+
|
|
43
|
+
/// Opaque identity stable for this source's current lifetime; not its HTML id.
|
|
44
|
+
public let sourceID: String
|
|
45
|
+
/// getBoundingClientRect(), in CSS pixels relative to the Web viewport.
|
|
46
|
+
public let sourceFrame: CGRect
|
|
47
|
+
/// The same source rectangle in WKWebView-local UIKit points, not screen coordinates.
|
|
48
|
+
public let sourceFrameInWebView: CGRect
|
|
49
|
+
/// CSS viewport width used to convert the snapshot into native coordinates.
|
|
50
|
+
public let viewportWidth: CGFloat
|
|
51
|
+
/// UIKit points per CSS pixel. This is not the device's pixel density.
|
|
52
|
+
public let pointsPerCSSPixel: CGFloat
|
|
53
|
+
public let placement: Placement
|
|
54
|
+
public let colorScheme: ColorScheme
|
|
55
|
+
public let layoutDirection: LayoutDirection
|
|
56
|
+
|
|
57
|
+
init(node: NativeUIControl, viewportWidth: CGFloat, pointsPerCSSPixel: CGFloat) {
|
|
58
|
+
sourceID = node.id
|
|
59
|
+
sourceFrame = node.frame.rect
|
|
60
|
+
sourceFrameInWebView = node.frame.rect.applying(
|
|
61
|
+
CGAffineTransform(scaleX: pointsPerCSSPixel, y: pointsPerCSSPixel))
|
|
62
|
+
self.viewportWidth = viewportWidth
|
|
63
|
+
self.pointsPerCSSPixel = pointsPerCSSPixel
|
|
64
|
+
placement = node.extensionContent?.tabBarId == nil ? .element : .tabAccessory
|
|
65
|
+
colorScheme = node.dark ? .dark : .light
|
|
66
|
+
layoutDirection = node.rtl ? .rightToLeft : .leftToRight
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
public enum NativeUIError: Error {
|
|
71
|
+
case duplicateRegistration, unavailable, rejected, timedOut
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Completion means the original Web element received its click, not that the
|
|
75
|
+
/// application's asynchronous work triggered by that click has finished.
|
|
76
|
+
@MainActor public struct NativeUIActions {
|
|
77
|
+
let dispatch: (String) async throws -> Void
|
|
78
|
+
/// Names of currently visible, enabled DOM targets supplied by the reader.
|
|
79
|
+
public let availableNames: Set<String>
|
|
80
|
+
|
|
81
|
+
init(availableNames: Set<String> = [], dispatch: @escaping (String) async throws -> Void) {
|
|
82
|
+
self.availableNames = availableNames
|
|
83
|
+
self.dispatch = dispatch
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public func isAvailable(_ name: String) -> Bool { availableNames.contains(name) }
|
|
87
|
+
|
|
88
|
+
public func activate(_ name: String) async throws {
|
|
89
|
+
try Task.checkCancellation()
|
|
90
|
+
try await dispatch(name)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
struct NativeUIExtensionContent: Decodable, Equatable {
|
|
95
|
+
let name: String
|
|
96
|
+
let props: String
|
|
97
|
+
let actions: [String: String]
|
|
98
|
+
let tabBarId: String?
|
|
99
|
+
|
|
100
|
+
var isValid: Bool {
|
|
101
|
+
!name.isEmpty && props.utf8.count <= 262144 && actions.count <= 30 &&
|
|
102
|
+
actions.allSatisfy { !$0.key.isEmpty && !$0.value.isEmpty } &&
|
|
103
|
+
(tabBarId.map { !$0.isEmpty } ?? true) &&
|
|
104
|
+
(try? JSONSerialization.jsonObject(with: Data(props.utf8))) is [String: Any]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/// Owns pending native -> Web calls independently of the View's lifetime.
|
|
109
|
+
@MainActor final class NativeUIExtensionActions {
|
|
110
|
+
private struct Pending {
|
|
111
|
+
let owner: String
|
|
112
|
+
let continuation: CheckedContinuation<Void, Error>
|
|
113
|
+
let deadline: DispatchWorkItem
|
|
114
|
+
}
|
|
115
|
+
private var pending: [String: Pending] = [:]
|
|
116
|
+
|
|
117
|
+
func perform(owner: String, timeout: TimeInterval = 4, send: (String) -> Void) async throws {
|
|
118
|
+
let request = UUID().uuidString
|
|
119
|
+
try await withTaskCancellationHandler {
|
|
120
|
+
try Task.checkCancellation()
|
|
121
|
+
try await withCheckedThrowingContinuation { continuation in
|
|
122
|
+
let deadline = DispatchWorkItem { [weak self] in self?.finish(request, error: NativeUIError.timedOut) }
|
|
123
|
+
pending[request] = Pending(owner: owner, continuation: continuation, deadline: deadline)
|
|
124
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + timeout, execute: deadline)
|
|
125
|
+
send(request)
|
|
126
|
+
}
|
|
127
|
+
} onCancel: {
|
|
128
|
+
Task { @MainActor [weak self] in self?.finish(request, error: CancellationError()) }
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
func finish(_ request: String, error: Error? = nil) {
|
|
133
|
+
guard let call = pending.removeValue(forKey: request) else { return }
|
|
134
|
+
call.deadline.cancel()
|
|
135
|
+
if let error { call.continuation.resume(throwing: error) }
|
|
136
|
+
else { call.continuation.resume() }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
func cancel(owner: String? = nil) {
|
|
140
|
+
for (request, call) in pending where owner == nil || call.owner == owner {
|
|
141
|
+
finish(request, error: NativeUIError.unavailable)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Normal UIViewController containment keeps SwiftUI appearance and teardown intact.
|
|
147
|
+
@MainActor final class NativeUIExtensionController {
|
|
148
|
+
let controller: UIHostingController<AnyView>
|
|
149
|
+
var view: UIView { controller.view }
|
|
150
|
+
|
|
151
|
+
init(content: AnyView, owner: UIViewController) {
|
|
152
|
+
controller = UIHostingController(rootView: content)
|
|
153
|
+
controller.view.backgroundColor = .clear
|
|
154
|
+
if #available(iOS 16.4, *) { controller.safeAreaRegions = [] }
|
|
155
|
+
owner.addChild(controller)
|
|
156
|
+
// Capacitor can use WKWebView as its controller's root view. A sibling
|
|
157
|
+
// shell surface is outside that controller's containment hierarchy.
|
|
158
|
+
owner.view.addSubview(controller.view)
|
|
159
|
+
controller.didMove(toParent: owner)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func update(_ content: AnyView) { controller.rootView = content }
|
|
163
|
+
func detach(duration: TimeInterval) {
|
|
164
|
+
controller.willMove(toParent: nil)
|
|
165
|
+
NativeUICrossfade.retire(controller.view, duration: duration)
|
|
166
|
+
controller.removeFromParent()
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
@available(iOS 26.0, *)
|
|
171
|
+
struct NativeUIExtensionAccessory: ViewModifier {
|
|
172
|
+
let content: AnyView?
|
|
173
|
+
func body(content base: Content) -> some View {
|
|
174
|
+
if #available(iOS 26.1, *) {
|
|
175
|
+
base.tabViewBottomAccessory(isEnabled: content != nil) { content }
|
|
176
|
+
} else { base }
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import UIKit
|
|
2
|
+
|
|
3
|
+
/// Image returned by the TypeScript read context's image() helper.
|
|
4
|
+
public struct NativeUIImage: Decodable {
|
|
5
|
+
public let data: String
|
|
6
|
+
public let width: Double
|
|
7
|
+
public let height: Double
|
|
8
|
+
public let template: Bool
|
|
9
|
+
|
|
10
|
+
public init(data: String, width: Double, height: Double, template: Bool = false) {
|
|
11
|
+
self.data = data
|
|
12
|
+
self.width = width
|
|
13
|
+
self.height = height
|
|
14
|
+
self.template = template
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Conversion primitives shared by standard controls and app-local extensions.
|
|
19
|
+
public enum NativeUIRendering {
|
|
20
|
+
/// Decodes base64 PNG artwork at its declared CSS size. Returns nil for invalid data.
|
|
21
|
+
public static func image(_ image: NativeUIImage) -> UIImage? {
|
|
22
|
+
guard image.width.isFinite, image.height.isFinite, image.width > 0, image.height > 0,
|
|
23
|
+
let bytes = Data(base64Encoded: image.data), let raw = UIImage(data: bytes), let cg = raw.cgImage else { return nil }
|
|
24
|
+
return UIImage(cgImage: cg, scale: CGFloat(cg.width) / image.width, orientation: .up)
|
|
25
|
+
.withRenderingMode(image.template ? .alwaysTemplate : .alwaysOriginal)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/// Converts computed rgb()/rgba() colors used by standard readers. Other CSS formats use the fallback.
|
|
29
|
+
public static func color(_ css: String?, fallback: UIColor = .label) -> UIColor {
|
|
30
|
+
guard let css, css.hasPrefix("rgb(") || css.hasPrefix("rgba("), !css.contains("%") else { return fallback }
|
|
31
|
+
let numbers = css.components(separatedBy: CharacterSet(charactersIn: "0123456789.-").inverted).compactMap(Double.init)
|
|
32
|
+
guard (numbers.count == 3 || numbers.count == 4), numbers.allSatisfy(\.isFinite) else { return fallback }
|
|
33
|
+
return UIColor(red: min(255, max(0, numbers[0])) / 255,
|
|
34
|
+
green: min(255, max(0, numbers[1])) / 255,
|
|
35
|
+
blue: min(255, max(0, numbers[2])) / 255,
|
|
36
|
+
alpha: numbers.count == 4 ? min(1, max(0, numbers[3])) : 1)
|
|
37
|
+
}
|
|
38
|
+
}
|