Status: unmaintained experiment — archived.
This repository was an experiment and is no longer maintained. It receives no fixes, no dependency updates, and no support, and it is being archived as read-only. Do not use it in production. It is left public only as a reference for the technique described below.
Tabbed, multi-instance web browsing inside a React Native app (iOS only), where
pages can open real child windows — window.open(), target="_blank" — and
each child becomes its own tab while keeping its live JavaScript relationship to
the opener.
That last part is the whole point. The simpler and more common approach is to report
the requested URL to JavaScript and load it into a freshly created web view. That is
the right default for "open this link somewhere else", but it means the WKWebView
WebKit already created for the request goes unused, along with window.opener, the
WindowProxy the opener holds, the shared WKWebViewConfiguration (process pool,
cookies, data store), and any non-GET navigation. Scripting between the two pages is
then impossible:
// example/www/index.html — this only works if the child web view is
// the one WebKit created, not a look-alike loading the same URL.
window.newWindow = window.open('http://localhost:8082/child.html', '_blank');
window.newWindow.postMessage('Hello from parent!', '*');This experiment kept that web view instead of discarding it: WebKit's child
WKWebView is created synchronously, parked in a native registry, then adopted
by the React Native view that mounts for the new tab.
Three pieces:
index.js—MultiWebView, areact-native-tab-viewTabViewwhose scenes arereact-native-webviewWebViews. Each one usesnativeConfigto swap in this library's native component, and passes apremadeWebViewIdentifierprop plus anonCreateWebViewevent handler.ios/AEMultiWebView.m—AEMultiWebView, a subclass ofRNCWebViewImplthat overrides theWKUIDelegatenew-window hook anddidMoveToWindow.ios/AEMultiWebViewManager.m— the view manager, doubling as the registry: anNSMutableDictionary<NSNumber *, WKWebView *>keyed by a randomlockIdentifier.
The handoff:
sequenceDiagram
participant WK as WebKit
participant T0 as Tab 0 view<br/>(AEMultiWebView)
participant Reg as Registry<br/>(AEMultiWebViewManager)
participant JS as MultiWebView (JS)
Note over WK: the page in tab 0 calls window.open()
WK->>T0: createWebViewWithConfiguration:
Note over T0: must return a live WKWebView now —<br/>cannot wait for a JS round-trip
T0->>T0: alloc a child web view with WebKit's configuration
T0->>Reg: park it under a random lockIdentifier
T0->>JS: onCreateWebView { url, lockIdentifier }<br/>(async, over the bridge)
T0-->>WK: return the child web view
Note over WK: window.open() resolves to a real WindowProxy
create participant T1 as Tab 1 view<br/>(AEMultiWebView)
JS->>T1: render a tab with<br/>premadeWebViewIdentifier = lockIdentifier
T1->>Reg: didMoveToWindow: claim that lockIdentifier
Reg-->>T1: the parked child web view
T1->>T1: install it before super's `_webView == nil` guard,<br/>re-wire delegates, addSubview
Note over T1: tab 1 now displays the web view WebKit<br/>created for the opener, so the two pages<br/>can still script each other
The synchronous return is the constraint that shapes everything.
-webView:createWebViewWithConfiguration:forNavigationAction:windowFeatures:
must hand WebKit a web view now; there is no way to suspend it while a React
Native render round-trips through the bridge. So the native side creates the web
view eagerly, keys it by lockIdentifier, and tells JS about it. JS renders a
tab whenever it gets around to it, and the mounting native view claims the
pre-made web view out of the registry rather than creating its own. The
identifier is the lock: whoever holds it can take the web view exactly once.
RNCWebViewImpl keeps its web view in a private property declared copy:
// react-native-webview 14.0.1, apple/RNCWebViewImpl.m:124
@property (nonatomic, copy) RNCWKWebView *webView;WKWebView does not conform to NSCopying, so assigning an existing web view
through that setter would send it -copyWithZone: and crash. Changing that one
attribute to strong is the only modification this library needs from
react-native-webview, applied as a patch at install time
(example/.yarn/patches/react-native-webview-npm-14.0.1-*.patch):
-@property (nonatomic, copy) RNCWKWebView *webView;
+@property (nonatomic, strong) RNCWKWebView *webView;Still copy on master (16.0.0), so the same one-word change applies to current
versions.
react-native-webview handles new-window requests with its onOpenWindow prop:
createWebViewWithConfiguration: reports the requested URL to JavaScript and returns
nil to WebKit. That is a sound design for the common case — the app decides what to
do with the URL (push a screen, open Safari, load it in the same view) and the library
never has to own a web view WebKit created.
The tradeoff is that returning nil means no child window exists, so window.open()
evaluates to null in the page: no WindowProxy for the opener, no window.opener
for the child, no postMessage between them. For opening a link elsewhere that costs
nothing; for a browser-like UI where pages script the windows they open, it is the
entire feature.
AEMultiWebView overrides createWebViewWithConfiguration: and returns a real web
view, so that upstream implementation never runs here — the two approaches are
alternatives, not layers.
The subclass overrides the new-window hook, but it inherits
decidePolicyForNavigationAction: unchanged, and that method short-circuits when the
prop is set (apple/RNCWebViewImpl.m:1365-1377):
if (_onOpenWindow && !hasTargetFrame) {
// ...
decisionHandler(WKNavigationActionPolicyCancel);
_onOpenWindow(event);
return;
}Cancelling the navigation means WebKit never asks for a child web view, so the
override never runs and nothing is ever parked. This library therefore leaves
onOpenWindow unset and reports new windows through its own onCreateWebView event.
Tab 0 opens a child window (child.html) with window.open() and posts a message
through the returned handle; tab 1 — the adopted web view — receives it.
A normal RN 0.80 app:
cd example
yarn # installs deps; the react-native-webview patch applies here
yarn www # serves the fixture pages on :8082 — leave this running
yarn ios # gems, pods, build, metro, simulator- Needs the one-word patch above. Without it, assigning the adopted web view
sends
-copyWithZone:to aWKWebView. - The New Architecture is not supported. The technique relies on
nativeConfigplus a customRCTViewManagersubclass, so the example setsENV['RCT_NEW_ARCH_ENABLED'] = '0'. React Native 0.81 is the last version that can run the old architecture — 0.82 and later hardcode new-arch-only, so this code has a hard ceiling. - iOS only. Android's
WebChromeClient.onCreateWindowhas an equivalent transport (WebView.WebViewTransport), but none of it was written. - The registry leaks any parked web view whose tab never mounts.
- Adopted tabs skip some of
RNCWebViewImpl's setup, since much of it reads private ivars: no pull-to-refresh, menu items, indicator styling, or content inset handling, and RNW's own JS↔nativepostMessagebridge is not wired for adopted views. Page-to-pagepostMessage— the point of the experiment — is unaffected. - Child windows share the opener's
WKWebViewConfiguration, so closing a child tab runsRNCWebViewImpl's teardown against the shareduserContentController.
