|
| 1 | +--- |
| 2 | +title: Multiple Windows |
| 3 | +description: Develop with multiple windows on supported devices. |
| 4 | +contributors: |
| 5 | + - NathanWalker |
| 6 | +--- |
| 7 | + |
| 8 | +NativeScript 9 adds first-class support for iOS multi-window (multi-scene) applications by adopting the UIScene lifecycle when enabled. This guide explains how to enable scenes, how NativeScript integrates with them, and how to work with scene-specific APIs and events. |
| 9 | + |
| 10 | +:::tip Why this matters |
| 11 | +Apple is moving all iOS apps to the UIScene lifecycle. Enabling scenes now makes your app future‑proof and unlocks multiple windows on iPadOS and visionOS. |
| 12 | +::: |
| 13 | + |
| 14 | +## Supported platforms |
| 15 | + |
| 16 | +- iPad running iPadOS 13 or later (multi-window capable) |
| 17 | +- visionOS (Vision Pro) |
| 18 | +- iPhone: runs with UIScene lifecycle; multiple windows are not exposed to users, but adopting UIScene is recommended |
| 19 | + |
| 20 | +## Prerequisites |
| 21 | + |
| 22 | +- NativeScript 9+ |
| 23 | +- iOS 13+ runtime |
| 24 | +- Xcode/iOS tooling capable of building with UIScene (Xcode 11+) |
| 25 | + |
| 26 | +## Enable scene lifecycle (Info.plist) |
| 27 | + |
| 28 | +NativeScript will automatically switch to UIScene lifecycle when a scene manifest is present in your iOS app `Info.plist`. Add the following keys: |
| 29 | + |
| 30 | +```xml |
| 31 | +<key>UIApplicationSceneManifest</key> |
| 32 | +<dict> |
| 33 | + <key>UIApplicationPreferredDefaultSceneSessionRole</key> |
| 34 | + <string>UIWindowSceneSessionRoleApplication</string> |
| 35 | + <key>UIApplicationSupportsMultipleScenes</key> |
| 36 | + <true/> |
| 37 | + <key>UISceneConfigurations</key> |
| 38 | + <dict> |
| 39 | + <key>UIWindowSceneSessionRoleApplication</key> |
| 40 | + <array> |
| 41 | + <dict> |
| 42 | + <key>UISceneConfigurationName</key> |
| 43 | + <string>Default Configuration</string> |
| 44 | + <key>UISceneDelegateClassName</key> |
| 45 | + <string>SceneDelegate</string> |
| 46 | + </dict> |
| 47 | + </array> |
| 48 | + </dict> |
| 49 | +</dict> |
| 50 | +``` |
| 51 | + |
| 52 | +When this configuration is detected, NativeScript adopts UIScene; on devices that don’t support scenes, your app continues to behave as a single-window app. |
| 53 | + |
| 54 | +:::note iPhone and UIScene |
| 55 | +Even on iPhone, adding the manifest switches your app to UIScene lifecycle. Xcode may show warnings like “UIScene lifecycle will soon be required” — using the manifest addresses this. |
| 56 | +::: |
| 57 | + |
| 58 | +## How it works in NativeScript |
| 59 | + |
| 60 | +When the scene manifest is present: |
| 61 | + |
| 62 | +- A `SceneDelegate` (exposed to iOS as `SceneDelegate`) implements `UIWindowSceneDelegate` to integrate scenes with NativeScript’s application runtime. |
| 63 | +- A UIWindow is created per `UIWindowScene` and mapped internally, preserving compatibility with traditional app lifecycle APIs. |
| 64 | +- NativeScript fires scene-specific events and forwards core application lifecycle events from a primary scene to maintain compatibility with existing code. |
| 65 | + |
| 66 | +### Scene lifecycle events |
| 67 | + |
| 68 | +Use the `SceneEvents` constants to subscribe to scene lifecycle changes: |
| 69 | + |
| 70 | +```ts |
| 71 | +export const SceneEvents = { |
| 72 | + sceneWillConnect: 'sceneWillConnect', |
| 73 | + sceneDidActivate: 'sceneDidActivate', |
| 74 | + sceneWillResignActive: 'sceneWillResignActive', |
| 75 | + sceneWillEnterForeground: 'sceneWillEnterForeground', |
| 76 | + sceneDidEnterBackground: 'sceneDidEnterBackground', |
| 77 | + sceneDidDisconnect: 'sceneDidDisconnect', |
| 78 | + sceneContentSetup: 'sceneContentSetup', |
| 79 | +}; |
| 80 | +``` |
| 81 | + |
| 82 | +Event payloads include scene and window references: |
| 83 | + |
| 84 | +```ts |
| 85 | +/** iOS event data for UIScene lifecycle (iOS 13+). */ |
| 86 | +export interface SceneEventData extends ApplicationEventData { |
| 87 | + /** The UIWindowScene instance associated with this event. */ |
| 88 | + scene?: UIWindowScene; |
| 89 | + /** The UIWindow for this scene (if applicable). */ |
| 90 | + window?: UIWindow; |
| 91 | + /** Scene connection options (for sceneWillConnect). */ |
| 92 | + connectionOptions?: UISceneConnectionOptions; |
| 93 | + /** Additional user info from the notification. */ |
| 94 | + userInfo?: NSDictionary<any, any>; |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +### iOSApplication scene APIs |
| 99 | + |
| 100 | +When UIScene is active, `Application.ios` exposes helpers for inspecting and controlling scenes and windows: |
| 101 | + |
| 102 | +- `supportsScenes(): boolean` — iOS supports UIScene (iOS 13+) |
| 103 | +- `supportsMultipleScenes(): boolean` — app can present multiple scenes/windows (iPadOS; typically false on iPhone and some simulators) |
| 104 | +- `getAllWindows(): UIWindow[]` — all app windows across scenes |
| 105 | +- `getAllScenes(): UIScene[]` — all attached scenes |
| 106 | +- `getWindowScenes(): UIWindowScene[]` — filtered to window scenes |
| 107 | +- `getPrimaryWindow(): UIWindow | undefined` — the primary window (for compatibility) |
| 108 | +- `getPrimaryScene(): UIWindowScene | undefined` — the primary scene |
| 109 | +- `isUsingSceneLifecycle(): boolean` — whether UIScene lifecycle is active |
| 110 | +- `setWindowRootView(window: UIWindow, view: View): void` — set NativeScript root view for a given scene’s window |
| 111 | + |
| 112 | +## Usage |
| 113 | + |
| 114 | +### Listen to scene events |
| 115 | + |
| 116 | +```ts |
| 117 | +import { Application, SceneEvents } from '@nativescript/core'; |
| 118 | + |
| 119 | +Application.on(SceneEvents.sceneWillConnect, (args) => { |
| 120 | + console.log('Scene connecting:', args.scene); |
| 121 | + console.log('Window:', args.window); |
| 122 | + console.log('Connection options:', args.connectionOptions); |
| 123 | +}); |
| 124 | + |
| 125 | +Application.on(SceneEvents.sceneDidActivate, (args) => { |
| 126 | + console.log('Scene active:', args.scene); |
| 127 | +}); |
| 128 | + |
| 129 | +Application.on(SceneEvents.sceneWillResignActive, (args) => { |
| 130 | + console.log('Scene will resign active:', args.scene); |
| 131 | +}); |
| 132 | + |
| 133 | +Application.on(SceneEvents.sceneWillEnterForeground, (args) => { |
| 134 | + console.log('Scene will enter foreground:', args.scene); |
| 135 | +}); |
| 136 | + |
| 137 | +Application.on(SceneEvents.sceneDidEnterBackground, (args) => { |
| 138 | + console.log('Scene entered background:', args.scene); |
| 139 | +}); |
| 140 | + |
| 141 | +Application.on(SceneEvents.sceneDidDisconnect, (args) => { |
| 142 | + console.log('Scene disconnected:', args.scene); |
| 143 | +}); |
| 144 | + |
| 145 | +Application.on(SceneEvents.sceneContentSetup, (args) => { |
| 146 | + // Create and attach NativeScript View content for the new scene here |
| 147 | + // See "Provide scene-specific UI" section below |
| 148 | + setupSceneContent(args); |
| 149 | +}); |
| 150 | +``` |
| 151 | + |
| 152 | +### Inspect and manage windows |
| 153 | + |
| 154 | +```ts |
| 155 | +import { Application } from '@nativescript/core'; |
| 156 | + |
| 157 | +if (Application.ios.supportsScenes()) { |
| 158 | + const windows = Application.ios.getAllWindows(); |
| 159 | + const scenes = Application.ios.getWindowScenes(); |
| 160 | + const primaryWindow = Application.ios.getPrimaryWindow(); |
| 161 | + |
| 162 | + console.log(`App has ${windows.length} windows`); |
| 163 | + console.log(`App has ${scenes.length} scenes`); |
| 164 | + console.log('Primary window:', primaryWindow); |
| 165 | + |
| 166 | + if (Application.ios.isUsingSceneLifecycle()) { |
| 167 | + console.log('Using UIScene lifecycle'); |
| 168 | + } |
| 169 | +} else { |
| 170 | + console.log('Single-window app lifecycle in effect'); |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +### Provide scene-specific UI |
| 175 | + |
| 176 | +```ts |
| 177 | +import { Application, Page, Utils } from '@nativescript/core'; |
| 178 | + |
| 179 | +function createPageForScene(scene: UIWindowScene, window: UIWindow): Page { |
| 180 | + // Construct any NativeScript view hierarchy here |
| 181 | + const page = new Page(); |
| 182 | + // ... add content |
| 183 | + return page; |
| 184 | +} |
| 185 | + |
| 186 | +export function setupSceneContent(args: SceneEventData) { |
| 187 | + // Optionally distinguish scenes by an id when opening a new window |
| 188 | + // (e.g., via NSUserActivity userInfo) |
| 189 | + let nsViewId: string | undefined; |
| 190 | + if (args.connectionOptions?.userActivities?.count > 0) { |
| 191 | + const activity = args.connectionOptions.userActivities.allObjects.objectAtIndex(0) as NSUserActivity; |
| 192 | + nsViewId = Utils.dataDeserialize(activity.userInfo).id; |
| 193 | + } |
| 194 | + |
| 195 | + let page: Page; |
| 196 | + switch (nsViewId) { |
| 197 | + case 'newSceneBasic': |
| 198 | + page = createPageForScene(args.scene, args.window); |
| 199 | + break; |
| 200 | + case 'newSceneAlt': |
| 201 | + page = createPageForScene(args.scene, args.window); // replace with alt page |
| 202 | + break; |
| 203 | + default: |
| 204 | + page = createPageForScene(args.scene, args.window); |
| 205 | + } |
| 206 | + |
| 207 | + Application.ios.setWindowRootView(args.window, page); |
| 208 | +} |
| 209 | +``` |
| 210 | + |
| 211 | +## Custom SceneDelegate (advanced) |
| 212 | + |
| 213 | +NativeScript ships a default `SceneDelegate` that integrates UIScene with the runtime and event system. If your application needs custom scene delegate behavior, you can provide your own implementation named `SceneDelegate` in your app and wire additional logic. Ensure that you continue to create a `UIWindow` per `UIWindowScene` and set the NativeScript root view to keep app behavior consistent. Most apps should prefer the default delegate. |
| 214 | + |
| 215 | +## Compatibility and behavior |
| 216 | + |
| 217 | +- Backwards compatibility: on devices or builds without a scene manifest, the traditional single-window lifecycle is used and existing apps continue to work unchanged. |
| 218 | +- Primary scene: for compatibility, NativeScript forwards core app lifecycle events (e.g., didBecomeActive) from the primary scene. |
| 219 | +- Multiple scenes: `supportsMultipleScenes()` is typically only true on physical iPadOS devices; it may return false on iPhone and some simulators. |
| 220 | + |
| 221 | +## Migration guidance |
| 222 | + |
| 223 | +Existing apps do not need to change code to adopt UIScene. To enable multi-window capabilities and Scene events: |
| 224 | + |
| 225 | +1. Add the scene manifest to `Info.plist` (see above). |
| 226 | +2. Listen to `SceneEvents` to tailor behavior per window. |
| 227 | +1. If you open additional windows, set their root views with `Application.ios.setWindowRootView` during `sceneContentSetup`. |
| 228 | + |
| 229 | +## Troubleshooting |
| 230 | + |
| 231 | +- “UIScene lifecycle will soon be required” in Xcode: add the scene manifest to `Info.plist` to adopt UIScene. |
| 232 | +- No multiple windows on iPhone: expected; iPhone uses UIScene lifecycle but doesn’t expose multi-window UX to users. |
| 233 | +- `supportsMultipleScenes()` returns false on simulator: test on a physical iPad where multi-window is supported. |
| 234 | + |
| 235 | +## Summary |
| 236 | + |
| 237 | +With UIScene enabled, NativeScript gives you: |
| 238 | + |
| 239 | +- Scene-aware events for window lifecycle handling |
| 240 | +- APIs to inspect scenes and windows and set scene-specific root views |
| 241 | +- Backwards-compatible behavior for apps that haven’t yet adopted scenes |
| 242 | + |
| 243 | +Use the examples above as a starting point to build multi-window workflows on iPadOS and visionOS while keeping your app ready for the future UIScene requirement on iOS. |
| 244 | + |
0 commit comments