Skip to content

fix(ios): re-attach root view after in-process runtime reload#11261

Open
NathanWalker wants to merge 1 commit into
mainfrom
fix/ios-reload
Open

fix(ios): re-attach root view after in-process runtime reload#11261
NathanWalker wants to merge 1 commit into
mainfrom
fix/ios-reload

Conversation

@NathanWalker
Copy link
Copy Markdown
Contributor

@NathanWalker NathanWalker commented Jun 6, 2026

When the runtime is reloaded in-process (NativeScriptRuntime.reloadApplication, used for OTA soft reboots) the app re-runs main in a fresh JS isolate while the process stays alive. iOSApplication.run() sees a live UIApplication and routes to runAsEmbeddedApp()

  • The previous UIWindow was deallocated along with the old isolate (it was retained only by the JS-side SceneDelegate._window), and sceneWillConnectToSession, which creates the window, does not re-fire on a reload. getWindow() then returned null and runAsEmbeddedApp() bailed early, leaving a black screen.
  • With no embedder delegate (NativeScript owns the UIApplication), the new root was presented modally over the old, now-dead root controller instead of replacing it, leaking the old controller on every reload and breaking when an overlay is mid-presentation.

runAsEmbeddedApp() works same as before but now supports this use case:

  • recreates a window bound to the active UIWindowScene when none is found (initWithWindowScene + _setWindowForScene/_setupWindowForScene);
  • moves the rootViewController guard into the embedder-only branch, since a freshly recreated window has no controller yet;
  • replaces the window root via setWindowRootView() for the NativeScript-owned case

Supports 9.1 runtime features for OTA installed hot reloading of runtime isolates.

When the runtime is reloaded in-process (NativeScriptRuntime.reloadApplication,
used for OTA soft reboots) the app re-runs main in a fresh JS isolate while the
process stays alive. iOSApplication.run() sees a live UIApplication and routes
to runAsEmbeddedApp()

- The previous UIWindow was deallocated along with the old isolate (it was
  retained only by the JS-side SceneDelegate._window), and
  sceneWillConnectToSession — which creates the window — does not re-fire on a
  reload. getWindow() then returned null and runAsEmbeddedApp() bailed early,
  leaving a black screen.
- With no embedder delegate (NativeScript owns the UIApplication), the new root
  was presented modally over the old, now-dead root controller instead of
  replacing it — leaking the old controller on every reload and breaking when an
  overlay is mid-presentation.

runAsEmbeddedApp() now:
- recreates a window bound to the active UIWindowScene when none is found
  (initWithWindowScene + _setWindowForScene/_setupWindowForScene);
- moves the rootViewController guard into the embedder-only branch, since a
  freshly recreated window has no controller yet;
- replaces the window root via setWindowRootView() for the NativeScript-owned
  case instead of modal-presenting over the stale root.
@nx-cloud
Copy link
Copy Markdown

nx-cloud Bot commented Jun 6, 2026

View your CI Pipeline Execution ↗ for commit bcb87c9

Command Status Duration Result
nx test apps-automated -c=android ✅ Succeeded 3m 38s View ↗
nx run-many --target=test --configuration=ci --... ✅ Succeeded <1s View ↗

💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗


☁️ Nx Cloud last updated this comment at 2026-06-06 00:17:54 UTC

@NathanWalker
Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Looking for one thing? Review this PR in Change Stack to search files, summaries, diffs, and code without losing your place.

Review Change Stack

📝 Walkthrough

Walkthrough

This PR updates embedded NativeScript app initialization to handle missing UIWindow by recreating it bound to an active UIWindowScene, tolerate null root view controller during startup, and adjust view attachment strategies based on embedder presence versus direct owning-app mode.

Changes

Embedded app window initialization

Layer / File(s) Summary
Window recreation and view controller attachment
packages/core/application/application.ios.ts
runAsEmbeddedApp now recreates UIWindow bound to selected UIWindowScene when unavailable, tolerates null rootViewController initially, and wires view controller views differently: embedder path guards missing controller and calls setViewControllerView before delegating; owning-app path attaches NativeScript directly via setWindowRootView instead of presenting from visible view controller.

Suggested labels

♥ community PR

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change: fixing iOS root view re-attachment after in-process runtime reload, which is the core issue addressed in the PR.
Description check ✅ Passed The description is directly related to the changeset, explaining the problem context, technical details, and specific changes made to runAsEmbeddedApp() to support in-process runtime reloading.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/ios-reload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: f2c1c242-f3f7-48bd-9baf-36cfebc2086a

📥 Commits

Reviewing files that changed from the base of the PR and between 71d2203 and bcb87c9.

📒 Files selected for processing (1)
  • packages/core/application/application.ios.ts

Comment on lines 388 to +393
if (embedderDelegate) {
// Embed into host app.
// present over the host's existing root view controller.
if (!rootController) {
return;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Partial initialization before early return may leave inconsistent state.

When embedderDelegate exists but rootController is null, the early return at line 392 occurs after rootView._setupAsRootView({}) (line 374) and event handler registration (lines 376-386) have already executed. This leaves this._rootView pointing to a view that was set up but never passed through initRootView() or notifyAppStarted().

Consider moving the rootController guard earlier (before _setupAsRootView and event handlers), or document that this partial state is intentionally handled on the next reload cycle.

🛠️ Suggested fix: Move guard earlier
+	const rootController = window.rootViewController;
+	const embedderDelegate = NativeScriptEmbedder.sharedInstance().delegate;
+
+	// Embed into host app requires an existing root view controller
+	if (embedderDelegate && !rootController) {
+		return;
+	}
+
 	const controller = this.getViewController(rootView);
-	const embedderDelegate = NativeScriptEmbedder.sharedInstance().delegate;

 	rootView._setupAsRootView({});

 	rootView.on(IOSHelper.traitCollectionColorAppearanceChangedEvent, () => {
 		const userInterfaceStyle = controller.traitCollection.userInterfaceStyle;
 		const newSystemAppearance = this.getSystemAppearanceValue(userInterfaceStyle);
 		this.setSystemAppearance(newSystemAppearance);
 	});

 	rootView.on(IOSHelper.traitCollectionLayoutDirectionChangedEvent, () => {
 		const layoutDirection = controller.traitCollection.layoutDirection;
 		const newLayoutDirection = this.getLayoutDirectionValue(layoutDirection);
 		this.setLayoutDirection(newLayoutDirection);
 	});

 	if (embedderDelegate) {
 		// Embed into host app.
 		// present over the host's existing root view controller.
-		if (!rootController) {
-			return;
-		}
 		this.setViewControllerView(rootView);
 		embedderDelegate.presentNativeScriptApp(controller);
 	} else {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant