@webview-native-ui/engine 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -14
- package/README.md +27 -599
- package/WebViewNativeUIEngine.podspec +2 -2
- package/dist/{scroll-bindings.d.ts → dom/scroll-bindings.d.ts} +1 -1
- package/dist/{scroll-placement.d.ts → dom/scroll-placement.d.ts} +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.js +8 -8
- package/dist/ionic.d.ts +3 -3
- package/dist/ionic.js +1 -1
- package/dist/{native-bootstrap.d.ts → native/bootstrap.d.ts} +1 -1
- package/dist/{native-bootstrap.js → native/bootstrap.js} +1 -1
- package/dist/{bridge.d.ts → native/bridge.d.ts} +1 -1
- package/dist/{native-session.d.ts → native/session.d.ts} +3 -2
- package/dist/{native-session.js → native/session.js} +8 -7
- package/dist/{projection-events.d.ts → projection/events.d.ts} +1 -1
- package/dist/{projection-events.js → projection/events.js} +1 -1
- package/dist/{web-projection.d.ts → projection/web.d.ts} +5 -3
- package/dist/{web-projection.js → projection/web.js} +9 -6
- package/dist/types.d.ts +1 -1
- package/docs/assets/hero.png +0 -0
- package/docs/capacitor-plugin.md +279 -0
- package/docs/cordova-plugin.md +198 -0
- package/docs/cordova.md +1 -123
- package/docs/maintenance.md +52 -0
- package/docs/projection.md +340 -0
- package/ios/Sources/WebViewNativeUIEngine/CoveredSnapshots.swift +6 -14
- package/ios/Sources/WebViewNativeUIEngine/NativeUIProjection.swift +6 -3
- package/ios/Sources/WebViewNativeUIEngine/Resources/native-ui-engine.js +3 -3
- package/package.json +2 -3
- package/NOTICE +0 -39
- /package/dist/{geometry.d.ts → dom/geometry.d.ts} +0 -0
- /package/dist/{geometry.js → dom/geometry.js} +0 -0
- /package/dist/{overlays.d.ts → dom/overlays.d.ts} +0 -0
- /package/dist/{overlays.js → dom/overlays.js} +0 -0
- /package/dist/{scroll-bindings.js → dom/scroll-bindings.js} +0 -0
- /package/dist/{scroll-placement.js → dom/scroll-placement.js} +0 -0
- /package/dist/{visibility.d.ts → dom/visibility.d.ts} +0 -0
- /package/dist/{visibility.js → dom/visibility.js} +0 -0
- /package/dist/{bridge.js → native/bridge.js} +0 -0
- /package/dist/{runtime.d.ts → native/resource-url.d.ts} +0 -0
- /package/dist/{runtime.js → native/resource-url.js} +0 -0
- /package/dist/{projection-placement.d.ts → projection/placement.d.ts} +0 -0
- /package/dist/{projection-placement.js → projection/placement.js} +0 -0
- /package/dist/{web-projection-scope.d.ts → projection/scope.d.ts} +0 -0
- /package/dist/{web-projection-scope.js → projection/scope.js} +0 -0
- /package/dist/{snapshot-styles.d.ts → projection/styles.d.ts} +0 -0
- /package/dist/{snapshot-styles.js → projection/styles.js} +0 -0
|
@@ -5,8 +5,8 @@ Pod::Spec.new do |s|
|
|
|
5
5
|
s.version = package['version']
|
|
6
6
|
s.summary = 'Native UI infrastructure for Capacitor and Cordova plugin maintainers.'
|
|
7
7
|
s.homepage = 'https://github.com/webview-native-ui/engine'
|
|
8
|
-
s.license = { :type => '
|
|
9
|
-
s.preserve_paths = 'LICENSE'
|
|
8
|
+
s.license = { :type => 'MIT', :file => 'LICENSE' }
|
|
9
|
+
s.preserve_paths = 'LICENSE'
|
|
10
10
|
s.author = { 'rdlabo' => 'sakakibara@rdlabo.jp' }
|
|
11
11
|
s.source = { :git => 'https://github.com/webview-native-ui/engine.git', :tag => "v#{s.version}" }
|
|
12
12
|
s.resource_bundles = { 'WebViewNativeUIEngine' => ['ios/Sources/WebViewNativeUIEngine/Resources/*.js'] }
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { WebProjectionSource } from '
|
|
1
|
+
import type { WebProjectionSource } from '../types.js';
|
|
2
2
|
/** Coordinate ownership, independent of any plugin's update transport or rendering. */
|
|
3
3
|
export declare const createScrollBindings: () => {
|
|
4
4
|
reuse: (candidate: WebProjectionSource, width: number) => boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ScrollPlacement as NativeUIScrollPlacement } from '
|
|
1
|
+
import type { ScrollPlacement as NativeUIScrollPlacement } from '../types.js';
|
|
2
2
|
type Identify = (element: HTMLElement) => string;
|
|
3
3
|
/** Layout-only measurement. Native owns subsequent scroll offsets. No private Ionic APIs. */
|
|
4
4
|
export declare const readScrollPlacement: (element: HTMLElement, owner: HTMLElement, id: Identify) => NativeUIScrollPlacement | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
|
-
export * from './geometry.js';
|
|
3
|
-
export * from './scroll-placement.js';
|
|
4
|
-
export { createWebProjectionScope, type WebProjectionScope } from './
|
|
5
|
-
export * from './web
|
|
6
|
-
export * from './scroll-bindings.js';
|
|
7
|
-
export type { ProjectionEvents, ProjectionDestination } from './projection
|
|
8
|
-
export { getOverlayCoordinator, usesOverlayBackdrop, type OverlayChange, type OverlayOptions } from './overlays.js';
|
|
9
|
-
export { getUnprojectedVisibility } from './visibility.js';
|
|
10
|
-
export { bindProjectionPlacement, type ProjectionPlacementBinding } from './projection
|
|
2
|
+
export * from './dom/geometry.js';
|
|
3
|
+
export * from './dom/scroll-placement.js';
|
|
4
|
+
export { createWebProjectionScope, type WebProjectionScope } from './projection/scope.js';
|
|
5
|
+
export * from './projection/web.js';
|
|
6
|
+
export * from './dom/scroll-bindings.js';
|
|
7
|
+
export type { ProjectionEvents, ProjectionDestination } from './projection/events.js';
|
|
8
|
+
export { getOverlayCoordinator, usesOverlayBackdrop, type OverlayChange, type OverlayOptions } from './dom/overlays.js';
|
|
9
|
+
export { getUnprojectedVisibility } from './dom/visibility.js';
|
|
10
|
+
export { bindProjectionPlacement, type ProjectionPlacementBinding } from './projection/placement.js';
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
|
-
export * from './geometry.js';
|
|
3
|
-
export * from './scroll-placement.js';
|
|
4
|
-
export { createWebProjectionScope } from './
|
|
5
|
-
export * from './web
|
|
6
|
-
export * from './scroll-bindings.js';
|
|
7
|
-
export { getOverlayCoordinator, usesOverlayBackdrop } from './overlays.js';
|
|
8
|
-
export { getUnprojectedVisibility } from './visibility.js';
|
|
9
|
-
export { bindProjectionPlacement } from './projection
|
|
2
|
+
export * from './dom/geometry.js';
|
|
3
|
+
export * from './dom/scroll-placement.js';
|
|
4
|
+
export { createWebProjectionScope } from './projection/scope.js';
|
|
5
|
+
export * from './projection/web.js';
|
|
6
|
+
export * from './dom/scroll-bindings.js';
|
|
7
|
+
export { getOverlayCoordinator, usesOverlayBackdrop } from './dom/overlays.js';
|
|
8
|
+
export { getUnprojectedVisibility } from './dom/visibility.js';
|
|
9
|
+
export { bindProjectionPlacement } from './projection/placement.js';
|
package/dist/ionic.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { type OverlayDefinition, type OverlayOptions } from './overlays.js';
|
|
2
|
-
export type { OverlayOptions as IonicOverlayOptions } from './overlays.js';
|
|
1
|
+
import { type OverlayDefinition, type OverlayOptions } from './dom/overlays.js';
|
|
2
|
+
export type { OverlayOptions as IonicOverlayOptions } from './dom/overlays.js';
|
|
3
3
|
/** Call once at app startup. Repeated default calls share the existing configuration and monitor. */
|
|
4
4
|
export declare const enableSupportIonicOverlays: (options?: OverlayOptions, doc?: Document | undefined) => {
|
|
5
5
|
readonly active: ReadonlySet<HTMLElement>;
|
|
6
|
-
subscribe(listener: (change: import("./overlays.js").OverlayChange) => void): () => void;
|
|
6
|
+
subscribe(listener: (change: import("./dom/overlays.js").OverlayChange) => void): () => void;
|
|
7
7
|
setNative(element: HTMLElement, value: boolean): void;
|
|
8
8
|
isExcluded(element: HTMLElement): boolean;
|
|
9
9
|
isNative(element: HTMLElement): boolean;
|
package/dist/ionic.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getOverlayCoordinator } from './overlays.js';
|
|
1
|
+
import { getOverlayCoordinator } from './dom/overlays.js';
|
|
2
2
|
const defaults = [
|
|
3
3
|
...['Modal', 'Popover', 'Alert', 'ActionSheet', 'Loading', 'Picker', 'Toast'].map((name) => ({
|
|
4
4
|
selector: `ion-${name.replace(/[A-Z]/g, (letter, index) => (index ? '-' : '') + letter.toLowerCase())}`,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type NativeSessionConfiguration } from './
|
|
1
|
+
import { type NativeSessionConfiguration } from './session.js';
|
|
2
2
|
/** Cordova plugins forward their existing notifications through NativeUIProjection.receive. */
|
|
3
3
|
export declare const notify: (id: string, name: string, data: unknown) => void | undefined;
|
|
4
4
|
export declare const receive: (id: string, key: string | null, value: any, error?: string) => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ListenerHandle as PluginListenerHandle } from '
|
|
1
|
+
import type { ListenerHandle as PluginListenerHandle } from '../types.js';
|
|
2
2
|
export declare const bounded: <T>(promise: Promise<T>) => Promise<T>;
|
|
3
3
|
/** A timed-out registration still owns a native resource if it eventually succeeds. */
|
|
4
4
|
export declare const boundedListener: (registration: Promise<PluginListenerHandle>) => Promise<PluginListenerHandle>;
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { type ProjectionDestination } from '
|
|
2
|
-
import type { ScrollPlacement, ListenerHandle, WebProjectionRequest } from '
|
|
1
|
+
import { type ProjectionDestination } from '../projection/events.js';
|
|
2
|
+
import type { ScrollPlacement, ListenerHandle, WebProjectionRequest } from '../types.js';
|
|
3
3
|
/** Serialized by the native library. One registration owns one fixed fallback container. */
|
|
4
4
|
export interface NativeSessionConfiguration {
|
|
5
5
|
id: string;
|
|
6
6
|
plugin: string;
|
|
7
7
|
runtime?: 'capacitor' | 'cordova';
|
|
8
8
|
projectionSelector: string;
|
|
9
|
+
captureDelayMs?: number;
|
|
9
10
|
/** Recovery while native is requested. Explicit web/none destinations take precedence. */
|
|
10
11
|
failedFallback?: 'web' | 'native';
|
|
11
12
|
captureMode?: 'composited' | 'view';
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { projectionPlacement } from '
|
|
2
|
-
import { getOverlayCoordinator } from '
|
|
3
|
-
import { readScrollPlacement } from '
|
|
4
|
-
import { projectionAncestors } from '
|
|
5
|
-
import { createWebProjection } from '
|
|
6
|
-
import { createWebProjectionScope } from '
|
|
7
|
-
import { bindProjectionEvents } from '
|
|
1
|
+
import { projectionPlacement } from '../projection/placement.js';
|
|
2
|
+
import { getOverlayCoordinator } from '../dom/overlays.js';
|
|
3
|
+
import { readScrollPlacement } from '../dom/scroll-placement.js';
|
|
4
|
+
import { projectionAncestors } from '../dom/geometry.js';
|
|
5
|
+
import { createWebProjection } from '../projection/web.js';
|
|
6
|
+
import { createWebProjectionScope } from '../projection/scope.js';
|
|
7
|
+
import { bindProjectionEvents } from '../projection/events.js';
|
|
8
8
|
/** Native startup calls this; application code does not initialize or wrap the plugin. */
|
|
9
9
|
export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
10
10
|
addListener: async (name, listener) => {
|
|
@@ -246,6 +246,7 @@ export const startNativeSession = async (config, nativeBridge, plugin = {
|
|
|
246
246
|
try {
|
|
247
247
|
const created = await createWebProjection(createWebProjectionScope(target, config.id), bridge, {
|
|
248
248
|
wake,
|
|
249
|
+
captureDelayMs: config.captureDelayMs,
|
|
249
250
|
blocked: () => destination !== 'native' || automaticMotion || (placementPending && !manager?.token),
|
|
250
251
|
revision: () => revision,
|
|
251
252
|
captureRevision: config.captureMode === 'view' ? () => contentRevision : undefined,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ListenerHandle } from '
|
|
1
|
+
import type { ListenerHandle } from '../types.js';
|
|
2
2
|
export type ProjectionDestination = 'native' | 'web' | 'none';
|
|
3
3
|
/** Notifications from the existing plugin proxy; public plugin methods are never intercepted. */
|
|
4
4
|
export interface ProjectionEvents {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { boundedListener } from '
|
|
1
|
+
import { boundedListener } from '../native/bridge.js';
|
|
2
2
|
/** Subscribe once per event, including events that both update and change destination. */
|
|
3
3
|
export const bindProjectionEvents = async (events, receive) => {
|
|
4
4
|
const routes = new Map();
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import type { WebProjectionSource, ScopedWebProjectionSource as Candidate, WebProjectionTransport } from '
|
|
2
|
-
import type { WebProjectionScope } from './
|
|
3
|
-
import { type ProjectionEvents } from './
|
|
1
|
+
import type { WebProjectionSource, ScopedWebProjectionSource as Candidate, WebProjectionTransport } from '../types.js';
|
|
2
|
+
import type { WebProjectionScope } from './scope.js';
|
|
3
|
+
import { type ProjectionEvents } from './events.js';
|
|
4
4
|
export interface WebProjectionOptions {
|
|
5
5
|
/** Existing plugin notifications, scoped to this presentation surface. */
|
|
6
6
|
events?: ProjectionEvents;
|
|
7
|
+
/** Debounce before preparing replacement pixels, in milliseconds. Defaults to 50; never delays handoff. */
|
|
8
|
+
captureDelayMs?: number;
|
|
7
9
|
wake(): void;
|
|
8
10
|
blocked(): boolean;
|
|
9
11
|
revision?(): number;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { projectionURL } from '
|
|
2
|
-
import { bounded, boundedListener } from '
|
|
3
|
-
import { bindProjectionEvents } from './
|
|
4
|
-
import { projectionAncestors } from '
|
|
5
|
-
import { snapshotStyles } from './
|
|
1
|
+
import { projectionURL } from '../native/resource-url.js';
|
|
2
|
+
import { bounded, boundedListener } from '../native/bridge.js';
|
|
3
|
+
import { bindProjectionEvents } from './events.js';
|
|
4
|
+
import { projectionAncestors } from '../dom/geometry.js';
|
|
5
|
+
import { snapshotStyles } from './styles.js';
|
|
6
6
|
const layout = (candidates, positionIndependent = false) => JSON.stringify(candidates.map(({ control: c }) => [
|
|
7
7
|
c.id,
|
|
8
8
|
c.kind,
|
|
@@ -51,6 +51,9 @@ const coordinateSpace = (target, win, style = win.getComputedStyle(target)) => {
|
|
|
51
51
|
};
|
|
52
52
|
/** Images are prepared after accepted native updates, never on the overlay's critical path. */
|
|
53
53
|
const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
54
|
+
const captureDelayMs = callbacks.captureDelayMs ?? 50;
|
|
55
|
+
if (!Number.isFinite(captureDelayMs) || captureDelayMs < 0 || captureDelayMs > 2147483647)
|
|
56
|
+
throw new RangeError('captureDelayMs must be a finite, nonnegative timer interval');
|
|
54
57
|
let destination = 'native';
|
|
55
58
|
const doc = scope.element.ownerDocument;
|
|
56
59
|
const win = doc.defaultView;
|
|
@@ -265,7 +268,7 @@ const createProjectionEngine = async (scope, transport, callbacks) => {
|
|
|
265
268
|
const schedule = () => {
|
|
266
269
|
win.clearTimeout(timer);
|
|
267
270
|
if (!stopped && destination === 'native' && !active && !touching && !callbacks.blocked())
|
|
268
|
-
timer = win.setTimeout(() => void prepare(),
|
|
271
|
+
timer = win.setTimeout(() => void prepare(), captureDelayMs);
|
|
269
272
|
};
|
|
270
273
|
let consideredLayout;
|
|
271
274
|
const consider = (revision, candidates) => {
|
package/dist/types.d.ts
CHANGED
|
Binary file
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Capacitor plugin integration
|
|
2
|
+
|
|
3
|
+
Add Web projection and restoration to an existing Capacitor plugin without changing its public `registerPlugin()` export. The plugin keeps its native renderer and application API; the Engine coordinates its native view with a DOM fallback.
|
|
4
|
+
|
|
5
|
+
This guide covers dependencies, registration, notifications and teardown. See [projection behavior](projection.md) for shared DOM requirements, capture policies and advanced integration.
|
|
6
|
+
|
|
7
|
+
## Requirements
|
|
8
|
+
|
|
9
|
+
This package declares support for Capacitor 8 (`>=8.0.0 <9`). The Engine library requires iOS 15+ and Android API 23+; the consuming Capacitor version or SDK may require a higher minimum. Composited Android capture requires API 26+ and the WebView features described in the [projection reference](projection.md#dom-and-native-surface-requirements).
|
|
10
|
+
|
|
11
|
+
## Install inside a plugin
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm install --save-exact @webview-native-ui/engine@0.0.1
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
The npm archive contains TypeScript declarations, ESM, Swift sources and a podspec. Native dependencies must also be declared by the consuming plugin; an npm dependency alone links neither the Swift library nor the Android AAR.
|
|
18
|
+
|
|
19
|
+
The primary native distribution is Swift Package Manager. Depend on the repository-root library from your plugin:
|
|
20
|
+
|
|
21
|
+
```swift
|
|
22
|
+
.package(url: "https://github.com/webview-native-ui/engine.git", exact: "0.0.1")
|
|
23
|
+
// In the plugin target's dependencies:
|
|
24
|
+
.product(name: "WebViewNativeUIEngine", package: "engine")
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The new coordinates start at `0.0.1`. While the API is pre-1.0, pin the same Engine release across npm, SwiftPM, Gradle and CocoaPods, and upgrade deliberately. Applications should retain their dependency lockfiles for reproducible builds.
|
|
28
|
+
|
|
29
|
+
SwiftPM resolves this repository's version tags such as `v0.0.1`. For unreleased integration tests, use a local checkout or an explicit tested commit.
|
|
30
|
+
|
|
31
|
+
`import WebViewNativeUIEngine` requires no Capacitor registration. The npm companion supplies DOM measurement and Web image handoff; SwiftPM resolves the native dependency independently of npm hoisting.
|
|
32
|
+
|
|
33
|
+
### Android: add the Maven dependency
|
|
34
|
+
|
|
35
|
+
In the consuming plugin's `android/build.gradle`, add the Engine version matching its npm dependency:
|
|
36
|
+
|
|
37
|
+
```groovy
|
|
38
|
+
dependencies {
|
|
39
|
+
implementation 'io.github.webview-native-ui:engine:0.0.1'
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Ensure `google()` and `mavenCentral()` are present in the consuming build's dependency repositories. For a build using centralized repositories, merge them into `android/settings.gradle`:
|
|
44
|
+
|
|
45
|
+
```groovy
|
|
46
|
+
dependencyResolutionManagement {
|
|
47
|
+
repositories {
|
|
48
|
+
google()
|
|
49
|
+
mavenCentral()
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
If the build uses project-level repositories instead, add them to its existing `repositories` block. Installing the npm package does not add this Gradle dependency. Use a version whose POM and AAR are available from Central; `PUBLISHING` only confirms acceptance and does not yet guarantee downloads.
|
|
55
|
+
|
|
56
|
+
### CocoaPods compatibility through npm
|
|
57
|
+
|
|
58
|
+
CocoaPods uses the Swift sources and podspec shipped in npm; no CocoaPods Trunk publication is required. The plugin maintainer declares the matching dependency in the plugin's podspec:
|
|
59
|
+
|
|
60
|
+
```ruby
|
|
61
|
+
s.dependency 'WebViewNativeUIEngine', '0.0.1'
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
The maintainer must also document this one-time application setup. Install the matching engine version as a direct npm dependency, then add the local pod **inside the application's target, outside `def capacitor_pods`**, which Capacitor regenerates:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
npm install --save-exact @webview-native-ui/engine@0.0.1
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
# ios/App/Podfile; adjust the path for a different native project layout.
|
|
72
|
+
target 'App' do
|
|
73
|
+
capacitor_pods
|
|
74
|
+
pod 'WebViewNativeUIEngine', :path => '../../node_modules/@webview-native-ui/engine'
|
|
75
|
+
end
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Run `npx cap sync ios` and rebuild. Keep the direct engine version aligned with the consuming plugin's dependency when upgrading. With a nonstandard node_modules layout, point `:path` at the installed engine package directory. An npm dependency alone does not register a Pod, and a podspec dependency cannot specify this local path. The engine intentionally has no Capacitor plugin registration or Podfile rewriting hook. SwiftPM consumers do not need this CocoaPods setup.
|
|
79
|
+
|
|
80
|
+
## Register from the native plugin
|
|
81
|
+
|
|
82
|
+
Keep the plugin's normal `registerPlugin()` export and public methods unchanged. Create and retain a registration after creating the native renderer. The engine ships its Web runtime with the Swift/Android library, starts it in the current document and future documents, finds the existing fallback DOM, and subscribes to the plugin's notifications. Applications do not call a separate JS initializer.
|
|
83
|
+
|
|
84
|
+
The integration has three responsibilities:
|
|
85
|
+
|
|
86
|
+
1. The consumer provides an existing, laid-out Web fallback container in the correct layer. Pass this projection destination as the required `projectionSelector: string`; there is no `null` / automatic DOM creation mode.
|
|
87
|
+
2. The plugin registers its native views and notification names once on each platform.
|
|
88
|
+
3. The plugin emits notifications after its updates and releases the registration at teardown.
|
|
89
|
+
|
|
90
|
+
### iOS: add the registration to your `CAPPlugin` class
|
|
91
|
+
|
|
92
|
+
In `ios/Sources/ExamplePlugin/ExamplePlugin.swift`, retain the registration as a property and initialize it in `load()`, **after your existing renderer creates and attaches its view**.
|
|
93
|
+
|
|
94
|
+
In this diff, `nativeView` is the `UIView` your plugin already owns; its existing rendering code is represented by comments.
|
|
95
|
+
|
|
96
|
+
Use the existing **`jsName`**, not `identifier`: Capacitor uses `jsName` for JS plugin calls and event subscriptions. Do not repeat the plugin name in the Engine configuration.
|
|
97
|
+
|
|
98
|
+
```diff
|
|
99
|
+
import Capacitor
|
|
100
|
+
import UIKit
|
|
101
|
+
+import WebViewNativeUIEngine
|
|
102
|
+
|
|
103
|
+
@objc(ExamplePlugin)
|
|
104
|
+
public class ExamplePlugin: CAPPlugin, CAPBridgedPlugin {
|
|
105
|
+
public let identifier = "ExamplePlugin"
|
|
106
|
+
public let jsName = "Example"
|
|
107
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
108
|
+
CAPPluginMethod(name: "setColors", returnType: CAPPluginReturnPromise)
|
|
109
|
+
]
|
|
110
|
+
private var nativeView: UIView?
|
|
111
|
+
+ private var projection: NativeUIProjection?
|
|
112
|
+
|
|
113
|
+
public override func load() {
|
|
114
|
+
// Existing code creates and attaches nativeView here.
|
|
115
|
+
+ do {
|
|
116
|
+
+ projection = try NativeUIProjection(
|
|
117
|
+
+ webView: bridge?.webView,
|
|
118
|
+
+ plugin: jsName,
|
|
119
|
+
+ projectionSelector: "#native-ui-fallback",
|
|
120
|
+
+ receiveListeners: .init(
|
|
121
|
+
+ update: ["appearanceChanged"],
|
|
122
|
+
+ projection: .init(
|
|
123
|
+
+ native: ["shown"],
|
|
124
|
+
+ web: ["transitionStarted"],
|
|
125
|
+
+ none: ["hidden"]
|
|
126
|
+
+ )
|
|
127
|
+
+ ),
|
|
128
|
+
+ view: { [weak self] in
|
|
129
|
+
+ self?.nativeView
|
|
130
|
+
+ }
|
|
131
|
+
+ )
|
|
132
|
+
+ } catch {
|
|
133
|
+
+ CAPLog.print("Native UI projection unavailable: \(error)")
|
|
134
|
+
+ }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@objc func setColors(_ call: CAPPluginCall) {
|
|
138
|
+
DispatchQueue.main.async {
|
|
139
|
+
// Existing code applies the requested colors to nativeView.
|
|
140
|
+
+ self.notifyListeners("appearanceChanged", data: [:])
|
|
141
|
+
call.resolve()
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
+
|
|
145
|
+
+ public override func notifyListeners(_ eventName: String, data: [String: Any]?, retainUntilConsumed: Bool) {
|
|
146
|
+
+ projection?.receive(eventName, data: data)
|
|
147
|
+
+ super.notifyListeners(eventName, data: data, retainUntilConsumed: retainUntilConsumed)
|
|
148
|
+
+ }
|
|
149
|
+
+
|
|
150
|
+
+ deinit {
|
|
151
|
+
+ projection?.destroy()
|
|
152
|
+
+ }
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Pass `bridge?.webView` directly. If the WebView is unavailable, the Engine throws an initialization error into the same `catch`; a separate guard or force unwrap is unnecessary.
|
|
157
|
+
|
|
158
|
+
If the view is created later in an existing `initialize` / `show` method, put the registration there after view creation instead of in `load()`. Retain one registration for that renderer's lifetime; do not create another on every update. When replacing a renderer, destroy its old registration before registering the replacement.
|
|
159
|
+
|
|
160
|
+
### Android: add the registration to your `Plugin` class
|
|
161
|
+
|
|
162
|
+
Make the corresponding changes in `android/src/main/java/<your-package>/ExamplePlugin.kt`. **`pluginHandle.id`** is Capacitor's registered plugin name, derived from `@CapacitorPlugin`; no second name string is needed.
|
|
163
|
+
|
|
164
|
+
```diff
|
|
165
|
+
import android.view.View
|
|
166
|
+
import com.getcapacitor.Plugin
|
|
167
|
+
import com.getcapacitor.PluginCall
|
|
168
|
+
import com.getcapacitor.PluginMethod
|
|
169
|
+
import com.getcapacitor.annotation.CapacitorPlugin
|
|
170
|
+
+import android.util.Log
|
|
171
|
+
+import com.getcapacitor.JSObject
|
|
172
|
+
+import io.github.webview_native_ui.engine.NativeUIProjection
|
|
173
|
+
+import io.github.webview_native_ui.engine.NativeUIProjection.ReceiveListeners
|
|
174
|
+
|
|
175
|
+
@CapacitorPlugin(name = "Example")
|
|
176
|
+
class ExamplePlugin : Plugin() {
|
|
177
|
+
private var nativeView: View? = null
|
|
178
|
+
+ private var projection: NativeUIProjection? = null
|
|
179
|
+
|
|
180
|
+
override fun load() {
|
|
181
|
+
// Existing code creates and attaches nativeView here.
|
|
182
|
+
+ projection = runCatching {
|
|
183
|
+
+ NativeUIProjection.install(bridge.webView)
|
|
184
|
+
+ NativeUIProjection(
|
|
185
|
+
+ window = activity.window,
|
|
186
|
+
+ webView = bridge.webView,
|
|
187
|
+
+ plugin = pluginHandle.id,
|
|
188
|
+
+ projectionSelector = "#native-ui-fallback",
|
|
189
|
+
+ receiveListeners = ReceiveListeners(
|
|
190
|
+
+ update = listOf("appearanceChanged"),
|
|
191
|
+
+ projection = ReceiveListeners.Projection(
|
|
192
|
+
+ native = listOf("shown"),
|
|
193
|
+
+ web = listOf("transitionStarted"),
|
|
194
|
+
+ none = listOf("hidden")
|
|
195
|
+
+ )
|
|
196
|
+
+ ),
|
|
197
|
+
+ view = { nativeView }
|
|
198
|
+
+ )
|
|
199
|
+
+ }.onFailure {
|
|
200
|
+
+ Log.e(javaClass.simpleName, "Native UI projection unavailable", it)
|
|
201
|
+
+ }.getOrNull()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
@PluginMethod
|
|
205
|
+
fun setColors(call: PluginCall) {
|
|
206
|
+
activity.runOnUiThread {
|
|
207
|
+
// Existing code applies the requested colors to nativeView.
|
|
208
|
+
+ notifyListeners("appearanceChanged", JSObject())
|
|
209
|
+
call.resolve()
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
+
|
|
213
|
+
+ override fun notifyListeners(eventName: String, data: JSObject, retainUntilConsumed: Boolean) {
|
|
214
|
+
+ projection?.receive(eventName, data)
|
|
215
|
+
+ super.notifyListeners(eventName, data, retainUntilConsumed)
|
|
216
|
+
+ }
|
|
217
|
+
+
|
|
218
|
+
+ override fun handleOnDestroy() {
|
|
219
|
+
+ projection?.close()
|
|
220
|
+
+ projection = null
|
|
221
|
+
+ super.handleOnDestroy()
|
|
222
|
+
+ }
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
On Android, call `NativeUIProjection.install(bridge.webView)` in plugin `load()`,
|
|
227
|
+
before the first navigation, even when SDK views arrive later. Include installation
|
|
228
|
+
in the same error boundary as registration; if it fails, keep the existing Web
|
|
229
|
+
fallback and do not retry installation after navigation. Registrations then
|
|
230
|
+
share that main-frame, origin-restricted channel; they do not install new message
|
|
231
|
+
listeners into a loaded WebView. This avoids a WebView native crash reproduced when
|
|
232
|
+
registering an asynchronously loaded AdMob view. The registration constructor can
|
|
233
|
+
install the channel only on an un-navigated WebView; late installation fails with
|
|
234
|
+
an actionable error instead of attempting unsafe injection. All registrations on
|
|
235
|
+
the same WebView must use the same allowed origins. Closing a registration removes
|
|
236
|
+
only its owner from the channel, so other slots continue working. The shared channel
|
|
237
|
+
remains installed for the WebView lifetime; registrations must still be retained
|
|
238
|
+
and closed by their consuming plugin.
|
|
239
|
+
|
|
240
|
+
Both diffs add a private property, native registration, a notification override, an update notification and teardown. They add no Engine methods to `pluginMethods`, no public JS wrapper, and no application-side Engine initializer.
|
|
241
|
+
|
|
242
|
+
Keep your plugin's existing `registerPlugin()` export. Use the same event names in the registration and `notifyListeners`; if the plugin already emits a suitable event (such as `tabSelected`), list that event instead of emitting it twice.
|
|
243
|
+
|
|
244
|
+
### Notifications during startup
|
|
245
|
+
|
|
246
|
+
The notification override above lets the Engine retain the latest matching projection destination before the Web runtime subscribes. No separate state variable or initial-state callback is needed. Forward every notification to `super` as usual: the Engine records state without replaying notifications, updates, or user actions. Without a destination notification, it infers the initial destination from native view visibility. Registration must exist before the notifications it should observe.
|
|
247
|
+
|
|
248
|
+
If your plugin owns multiple registrations, pass the notification to each registration; `receiveListeners.match` selects the owning surface. Appearance updates do not change the destination. The renderer still owns native placement and visibility before connection.
|
|
249
|
+
|
|
250
|
+
With the default Web fallback, scrolling without a prepared image or native scroll binding keeps the Web fallback visible until the renderer confirms current native placement with its native-destination notification. An appearance update alone is not that confirmation. Consumers choosing native failure recovery must keep their own native geometry current.
|
|
251
|
+
|
|
252
|
+
### Which events to emit
|
|
253
|
+
|
|
254
|
+
All incoming event declarations belong to `receiveListeners`. Its `update` list refreshes the image, its `projection` lists select the destination, and its optional `match` filters notifications for the owning surface.
|
|
255
|
+
|
|
256
|
+
| Notification | Where to emit it in the existing plugin |
|
|
257
|
+
| --------------------------------------- | ------------------------------------------------------------------------------------- |
|
|
258
|
+
| `appearanceChanged` (an `update` event) | After a successful native appearance update, such as `setColors` |
|
|
259
|
+
| `transitionStarted` (`web`) | Before the fallback DOM starts moving or a Web layer covers the native view |
|
|
260
|
+
| `shown` (`native`) | After the plugin's existing show operation, or when returning from the Web projection |
|
|
261
|
+
| `hidden` (`none`) | After the plugin's existing hide operation |
|
|
262
|
+
|
|
263
|
+
The names in the examples are conventions you choose, not reserved Engine methods. An `update` event refreshes the prepared image without changing the requested destination. It does not call `setColors` again.
|
|
264
|
+
|
|
265
|
+
Emit `transitionStarted` before the DOM starts moving, `shown` when native projection is requested again, and `hidden` after the plugin's own hide operation. Native capture, URL conversion, image preparation, draw acknowledgement, image retirement and listener cleanup stay inside the engine. The engine never invokes the plugin's `show`, `hide`, selection or ad-loading methods. End the registration with `projection.destroy()` on iOS or `projection.close()` on Android when the renderer is destroyed. Retain the registration for its full lifetime and call native APIs on the UI thread.
|
|
266
|
+
|
|
267
|
+
## DOM, scrolling and overlays
|
|
268
|
+
|
|
269
|
+
Provide one laid-out DOM container for each registration, in the layer where its Web projection belongs. It may be inserted after registration. The plugin still owns native view placement and sizing; the Engine does not create or size either view for you.
|
|
270
|
+
|
|
271
|
+
Read the [DOM and native surface requirements](projection.md#dom-and-native-surface-requirements) before integrating custom elements, SDK content or multiple surfaces. Scroll tracking and image handoff are internal to the registration. For Ionic applications, optionally enable [Ionic overlay support](projection.md#ionic-overlay-support-optional) once during application startup. Other frameworks emit the configured projection notifications from their own transition and overlay lifecycle.
|
|
272
|
+
|
|
273
|
+
## Animated native controls
|
|
274
|
+
|
|
275
|
+
Native appearance updates prepare replacement images after a 50 ms debounce by default.
|
|
276
|
+
If your control continues animating after its update notification, set `captureDelayMs`
|
|
277
|
+
on the native registration (Swift: `captureDelayMs: 600`; Kotlin: `captureDelayMs = 600`).
|
|
278
|
+
This changes background image preparation, not overlay presentation timing.
|
|
279
|
+
See [capture timing](projection.md#capture-timing) for the cache behavior and measured guidance.
|