Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 127 additions & 5 deletions packages/core/application/application.ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,16 @@ function supportsMultipleScenes(): boolean {
return UIApplication.sharedApplication?.supportsMultipleScenes;
}

/**
* Number of times the JS runtime has been soft-rebooted in this process via
* NativeScriptRuntime.reloadApplication / restartWithConfig. 0 on first boot.
* Provided as a global by the iOS runtime (v9+); older runtimes report 0.
*/
function getRuntimeReloadCount(): number {
const runtime = (globalThis as any).NativeScriptRuntime;
return runtime && typeof runtime.reloadCount === 'number' ? runtime.reloadCount : 0;
}

@NativeClass
class Responder extends UIResponder implements UIApplicationDelegate {
get window(): UIWindow {
Expand Down Expand Up @@ -269,6 +279,13 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication

private _notificationObservers: NotificationObserver[] = [];

// Strong references to delegates recreated after an in-process soft reboot
// (NativeScriptRuntime.reloadApplication). UIApplication.delegate is an
// `assign` property and UIScene keeps its own reference to the delegate we
// replace, so without these the fresh instances would be deallocated.
private _softRebootAppDelegate: UIApplicationDelegate;
private _softRebootSceneDelegates = new Map<UIScene, UIWindowSceneDelegate>();

displayedOnce = false;
displayedLinkTarget: CADisplayLinkTarget;
displayedLink: CADisplayLink;
Expand Down Expand Up @@ -323,6 +340,8 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
}

private runAsEmbeddedApp() {
this._reattachNativeDelegatesAfterSoftReboot();

// TODO: this rootView should be held alive until rootController dismissViewController is called.
const rootView = this.createRootView(this._rootView, true);
if (!rootView) {
Expand All @@ -331,19 +350,57 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
this._rootView = rootView;
setRootView(rootView);
// Attach to the existing iOS app
const window = getWindow() as UIWindow;
let window = getWindow() as UIWindow;

if (!window) {
// In-process soft reboot with OTAs.
// Original UIWindow is deallocated when the old JS isolate is torn down.
// Recreate a window bound to the active UIWindowScene so the
// root has somewhere to attach.
const app = UIApplication.sharedApplication;
const all = app && app.connectedScenes ? app.connectedScenes.allObjects : null;
let targetScene: UIWindowScene;
if (all) {
for (let i = 0; i < all.count; i++) {
const s = all.objectAtIndex(i) as UIWindowScene;
// Only UIWindowScene exposes `windows`; prefer a foreground-active one.
if (s && typeof s.windows !== 'undefined') {
targetScene = s;
if (s.activationState === UISceneActivationState.ForegroundActive) {
break;
}
}
}
}
if (targetScene) {
window = UIWindow.alloc().initWithWindowScene(targetScene);
this._setWindowForScene(window, targetScene);
this._setupWindowForScene?.(window, targetScene);

// If the scene's delegate was recreated after a soft reboot, point it
// at the new window so `scene.delegate.window` queries resolve.
const freshSceneDelegate = this._softRebootSceneDelegates.get(targetScene);
if (freshSceneDelegate) {
freshSceneDelegate.window = window;
}
}
}

if (!window) {
return;
}

// May be null on a freshly recreated window — expected; the replace-root
// path below sets it. Only the embedder path needs an existing controller.
const rootController = window.rootViewController;
Comment thread
NathanWalker marked this conversation as resolved.
if (!rootController) {
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({});

Expand All @@ -360,17 +417,82 @@ export class iOSApplication extends ApplicationCommon implements IiOSApplication
});

if (embedderDelegate) {
// Embed into host app.
// present over the host's existing root view controller.
this.setViewControllerView(rootView);
embedderDelegate.presentNativeScriptApp(controller);
} else {
const visibleVC = iosUtils.getVisibleViewController(rootController);
visibleVC.presentViewControllerAnimatedCompletion(controller, true, null);
// No embedder delegate = NativeScript owns the UIApplication.
// Attach the root to the window.
this.setViewControllerView(rootView);
this.setWindowRootView(window, rootView);
}

this.initRootView(rootView);
this.notifyAppStarted();
}

/**
* After an in-process soft reboot (NativeScriptRuntime.reloadApplication /
* restartWithConfig), the Objective-C delegate classes created by the
* previous JS isolate still exist and UIKit keeps dispatching to their
* now-inert instances: their method callbacks bail out because the isolate
* that implemented them is gone. Notification-center observers are
* re-registered by the new isolate, but delegate-based dispatch (custom
* UIApplicationDelegate methods like push-token/openURL callbacks, and the
* UIScene delegates used by scene-lifecycle apps) stays pinned to the old
* bundle. Recreate those delegates from this bundle's classes and re-point
* UIKit at them.
*/
private _reattachNativeDelegatesAfterSoftReboot(): void {
if (getRuntimeReloadCount() <= 0) {
// First boot: UIApplicationMain (or the host app) set up delegates.
return;
}

if (isEmbedded()) {
// The host app owns the UIApplication delegate; never touch it.
return;
}

const app = UIApplication.sharedApplication;
if (!app) {
return;
}

// Fresh application delegate from the new bundle. Assigning `delegate`
// does not retain (unlike the UIApplicationMain launch path), so keep a
// strong reference ourselves.
this.delegate ??= Responder as any;
const freshDelegate = (<any>this.delegate).new() as UIApplicationDelegate;
this._softRebootAppDelegate = freshDelegate;
app.delegate = freshDelegate;

// Re-point already-connected scenes at fresh scene delegates so scene
// lifecycle and user-implemented scene delegate methods (shortcuts,
// openURLContexts, userActivity continuation, etc.) reach this isolate.
// Newly connecting scenes are covered by the fresh application delegate's
// applicationConfigurationForConnectingSceneSessionOptions, which returns
// this bundle's SceneDelegate class.
if (this.supportsScenes()) {
this._softRebootSceneDelegates.clear();
const scenes = app.connectedScenes?.allObjects;
for (let i = 0; scenes && i < scenes.count; i++) {
const scene = scenes.objectAtIndex(i);
if (!(scene instanceof UIWindowScene)) {
continue;
}
const freshSceneDelegate = SceneDelegate.new() as UIWindowSceneDelegate;
scene.delegate = freshSceneDelegate;
this._softRebootSceneDelegates.set(scene, freshSceneDelegate);
}
}

if (Trace.isEnabled()) {
Trace.write(`Reattached application delegate${this._softRebootSceneDelegates.size ? ` and ${this._softRebootSceneDelegates.size} scene delegate(s)` : ''} after soft reboot (reloadCount: ${getRuntimeReloadCount()})`, Trace.categories.NativeLifecycle);
}
}

private getViewController(rootView: View): UIViewController {
let viewController: UIViewController = rootView.viewController || rootView.ios;

Expand Down
Loading