Streaming is available in most browsers,
and in the Developer app.
-
Make your UIKit app more flexible
Find out how your UIKit app can become more flexible on iPhone, iPad, Mac, and Apple Vision Pro by using scenes and container view controllers. Learn to unlock your app's full potential by transitioning from an app-centric to a scene-based lifecycle, including enhanced window resizing and improved multitasking. Explore enhancements to UISplitViewController, such as interactive column resizing and first-class support for inspector columns. And make your views and controls more adaptive by adopting new layout APIs.
Chapters
- 0:00 - Introduction
- 0:58 - Scenes
- 4:58 - Container view controllers
- 10:45 - Adaptivity
- 15:39 - Future compatibility
- 16:07 - Next steps
Resources
Related Videos
WWDC25
WWDC24
-
Search this video…
Hi there, welcome to “Make your UIKit app more flexible.” My name is Alexander MacLeod, and I'm an engineer on the UIKit team. A flexible app delivers an amazing experience across a variety of sizes and platforms. It maintains a familiar and intuitive navigation experience at any size. In this video, I will talk about some of the best practices to ensure that your app is flexible. First, I will go over the fundamentals of scenes, and share how they are foundational to a flexible application.
Next, I will cover container view controllers, such as UISplitViewController and UITabBarController, and explore how they bring flexibility to your app. Finally, I will talk about APIs to support you in building an adaptive, and truly flexible UI.
I’ll start with scenes.
A scene is an instance of your app’s UI. It contains your app’s view controllers and views. Scenes provide hooks for handling external data, like a URL for deep linking to a section of your app’s UI.
Each scene independently saves and restores UI state. A scene determines the best opportunities to ask for the current state, before persisting it to disk. You can query the previous UI state when a scene reconnects. This enables you to restore your scene exactly how it was before. Scenes also provide context on how your app is displayed, including details about the screen, and the window’s geometry.
You can have multiple scenes, each with their own lifecycle and state.
Dedicated scene types are designed to encapsulate distinct experiences. For example, a messaging app can have a dedicated compose scene for sending new messages. In iOS 26, you can now mix SwiftUI and UIKit scene types in a single app. Check out “What’s new in UIKit” for more. The portability that scenes provide are the perfect foundation for a flexible app.
As scenes are vital for ensuring flexibility, adopting UIScene life cycle will soon be mandatory. In the next major release following iOS 26, UIScene life cycle will be required when building with the latest SDK.
While supporting multiple scenes is encouraged, only the adoption of scene life cycle is required. For details on how to adopt UIScene life cycle, read the tech note: “Migrating to the UIKit scene-based life cycle.” Because scenes are so important, I will show you an example of them in practice. I have developed an app that tracks the time I spend on a particular task. It has a feature where I can AirPlay the current task to an Apple TV.
It is the responsibility of the app delegate to determine the scene configuration for a connecting session. In the configurationForConnecingSceneSession delegate method, I check the scene session’s role.
If the role is a non-interactive external display, I return a bespoke scene configuration. Otherwise, the main scene configuration is preferred. Each configuration is defined in the app's Info.plist file.
UISceneDelegate manages the life cycle of an individual scene.
In sceneWillConnectToSession, I first create a window, and associate it with the connecting scene. Note, if your scene configuration specifies a storyboard, window creation happens automatically.
I specify the window’s root view controller and provide it with scene-specific data, like the timer model.
For my app, it is important to pause the timer when the scene moves to the background. To achieve this, I implement the sceneDidEnterBackground delegate method and pause the timer.
I handle state restoration to ensure that the UI state of a connecting scene is exactly how it was left before.
My scene delegate provides a state restoration activity, which can include selections, navigation paths, and other UI state. The system persists this UI state, associating it with the scene instance. If the scene later reconnects, the state restoration activity is made available in the restoreInteractionStateWith userActivity delegate method. By populating the timer model with info from the user activity, I ensure that the UI state of the connecting scene is exactly how it was left before.
By adopting UIScene life cycle, I have strong foundations for a flexible application. Now, I will cover container view controllers, and explain how they are vital for building a flexible application. A container view controller is responsible for managing the layout of one or more child view controllers. UIKit provides a number of container view controllers that are designed to be flexible. First, I will talk about UISplitViewController.
UISplitViewController manages the display of multiple adjacent columns of content, supporting seamless navigation throughout a hierarchy of information. When horizontal space is limited, the split view controller adapts by collapsing its columns into a navigation stack. UISplitViewController gains a host of new features, starting with interactive column resizing.
You can now resize columns by dragging the split view controller’s separators. When using the pointer, its shape will adapt to indicate the directions in which a column can be resized. UISplitViewController provides a default minimum, maximum, and preferred width for each column.
There may be columns in your app that prefer displaying content at greater widths, or only require a fraction of the default width to remain functional. You can customize the minimum, maximum, and preferred widths of each column using their associated split view controller properties. Be careful not to require a width that limits the number of columns that can be displayed, as this reduces the flexibility of your app. Your UI may need to adapt depending on whether the split view controller is expanded or collapsed.
In Mail, disclosure indicators are shown when the split view controller is collapsed, to convey additional content can be revealed upon cell selection.
A new trait, split view controller layout environment, conveys whether an ancestor split view controller is expanded or collapsed. In this example, the trait is queried to conditionally add a disclosure indicator when the split view controller is collapsed. Also new, is first-class support for inspector columns.
An inspector is a column within a split view controller that provides additional details of the selected content. Preview uses an inspector to display metadata alongside the photo in the secondary column. When the split view controller is expanded, the inspector column resides on the trailing edge, adjacent to the secondary column.
When collapsed, the split view controller adapts automatically, and presents the inspector column as a sheet.
To incorporate an inspector in your split view controller, specify a view controller for the inspector column. When the split view controller first appears, the inspector column is hidden. Call show to display the inspector column. UISplitViewController is designed to be flexible, and will ensure that your app delivers the best navigation experience at any size.
Another container at your disposal is UITabBarController.
UITabBarController displays multiple, mutually exclusive panes of content, in the same area. The tab bar enables quick switching between tabs, while preserving the current state within each pane.
What’s more, the appearance of the tab bar adapts for each platform.
On iPhone, the tab bar is located at the bottom of the scene.
On Mac, the tab bar can reside in the toolbar or can be displayed as a sidebar.
On Apple Vision Pro, the tab bar is displayed in an ornament on the leading edge of the scene. On iPad, the tab bar resides at the top of the scene alongside navigation controls.
The tab bar can also adapt into a sidebar, allowing quick access to collections of content.
Tab groups surface additional destinations in the sidebar. For example, in the Music app on iPad, the Library tab group includes Artists, Albums, and more.
When the sidebar is not available, the Library group is a tab destination.
UITabBarController offers API to seamlessly manage this adaptation. First, provide the tab group with a managing navigation controller. When a leaf tab of the tab group is selected, its view controller, along with the view controllers of its ancestor groups, are pushed onto this navigation stack.
To customize the view controllers pushed onto this navigation stack, implement the UITabBarController delegate method, displayedViewControllersFor tab.
In this example, when the library tab cannot be selected, the delegate method returns an empty array to omit the library tab’s view controller from the stack.
For more on how UITabBarController offers flexibility to display a tab bar or sidebar, watch "Elevate your tab and sidebar experience in iPadOS” from WWDC24. Adopting container view controllers, such as UISplitViewController and UITabBarController, is the best way to ensure your app is flexible. While these containers are designed to support a wide range of sizes, your app may require a minimum size to maintain core functionality.
You can use the UISceneSizeRestrictions API to express the preferred minimum size of your scene’s content. The best time to specify the minimum size is when the scene is about to connect. In this example, I specify a preferred minimum width of 500 points.
For your app to be truly flexible, your own UI should be able to adapt. Next, I will talk about APIs that will support you in building an adaptive UI.
A crucial step in making your UI adaptable is to ensure that content remains within the safe area. The safe area is a region within a view that is appropriate for interactive or important content. Content placed outside of this region is vulnerable to getting covered, such as by a navigation bar or a toolbar.
Content could also be occluded by system UI like the status bar, or even device features, like the Dynamic Island.
The sidebar adds a non-symmetrical safe area inset to the adjacent column in a split view controller. The background can freely extend outside of the safe area, underneath the sidebar.
Content, such as the message transcript, is positioned within the safe area to ensure that is remains visible. The message bubbles are inset from the edges of the safe area using layout margins. This provides consistent spacing, and clear visual separation from the sidebar.
Each view provides layout guides to apply standard margins around content. Layout margins are inset from the safe area by default.
In this example, I request a layout guide for positioning content inside the container view.
I then use this layout guide to configure constraints for the content view.
In iPadOS 26, scenes gain a new control to close, minimize, and arrange the window, similar to macOS. The window control appears alongside the content in your scene.
A scene can specify a preferred windowing control style to compliment its content.
To specify a preference, implement the UIWindowSceneDelegate method preferredWindowingControlStyle for scene.
System components, such as UINavigationBar, adapt automatically by arranging their subviews around the window control. Your UI should also adapt to the window control, regardless of its style.
To ensure that your UI is not occluded, use a layout guide that accounts for the window control.
In this example, I request a layout margins guide with a horizontal corner adaptation.
This layout guide is great for bar-like content at the top of a scene, which should be inset from the trailing edge of the window control. I then use this layout guide to configure constraints for the content view. When your UI is adaptive, the interface orientation should be redundant. Scene resizing, device rotation, and changes to window layout, all ultimately result in a modification to your scene’s size. Certain categories of apps may benefit from temporarily locking the orientation. For example, a driving game may want to lock the orientation when the device is expected to rotate for steering a vehicle.
When a view controller is visible, it can prefer a locked interface orientation. To specify a preference, override prefersInterfaceOrientationLocked in your view controller subclass.
Whenever this preference changes, call setNeedsUpdateOfPrefersInterfaceOrientationLocked.
To observe the interface orientation lock, implement the UIWindowSceneDelegate method, didUpdateEffectiveGeometry. Then, compare whether the value of isInterfaceOrientationLocked has changed.
For your app to be truly adaptable, it should respond quickly to being resized. There may be elements of your app’s UI that are computationally expensive to draw.
This is common for games, where a number of assets may need to be resized when the scene changes size.
Re-rendering assets for every size within a resize interaction is unnecessary.
In this example, isInteractivelyResizing is queried to only update assets for a new scene size after the interaction finishes.
Flexible apps empower people to use their devices how they want. They provide great experiences across a wide range of sizes, allowing them to be used in any orientation or layout. The UIRequiresFullscreen Info.plist key is a compatibility mode from iOS 9 that prevents scene resizing. UIRequiresFullscreen is deprecated and will be ignored in a future release.
Apps that are adaptable do not need this key, and should remove it.
There is another compatibility mode, specifically for new hardware. Previously, when new hardware was released with a different screen size, the system would scale or letterbox your app’s UI. That scaling would stay in place until you built with a newer SDK and resubmitted your app.
Once you build and submit with the iOS 26 SDK, the system will no longer scale or letterbox your app’s UI for a new screen size.
These are the best practices to ensure that your app is flexible. So what’s next? Adopt scene life cycle in your app to ensure strong foundations for a flexible application. Use container view controllers to manage components of your UI. Finally, leverage APIs like layout guides to support you in building an adaptive UI. I can't wait to see your apps become more flexible. Thank you!
-
-
3:02 - Specify the scene configuration
// Specify the scene configuration @main class AppDelegate: UIResponder, UIApplicationDelegate { func application(_ application: UIApplication, configurationForConnecting sceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { if sceneSession.role == .windowExternalDisplayNonInteractive { return UISceneConfiguration(name: "Timer Scene", sessionRole: sceneSession.role) } else { return UISceneConfiguration(name: "Main Scene", sessionRole: sceneSession.role) } } }
-
3:30 - Configure the UI
// Configure the UI class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var timerModel = TimerModel() func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let windowScene = scene as! UIWindowScene let window = UIWindow(windowScene: windowScene) window.rootViewController = TimerViewController(model: timerModel) window.makeKeyAndVisible() self.window = window } }
-
3:56 - Handle life cycle events
// Handle life cycle events class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var timerModel = TimerModel() // ... func sceneDidEnterBackground(_ scene: UIScene) { timerModel.pause() } }
-
4:09 - Restore UI state
// Restore UI state class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var timerModel = TimerModel() // ... func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { let userActivity = NSUserActivity(activityType: "com.example.timer.ui-state") userActivity.userInfo = ["selectedTimeFormat": timerModel.selectedTimeFormat] return userActivity } func scene(_ scene: UIScene restoreInteractionStateWith userActivity: NSUserActivity) { if let selectedTimeFormat = userActivity?["selectedTimeFormat"] as? String { timerModel.selectedTimeFormat = selectedTimeFormat } }
-
4:46 - Adapt for the split view controller layout environment
// Adapt for the split view controller layout environment override func updateConfiguration(using state: UICellConfigurationState) { // ... if state.traitCollection.splitViewControllerLayoutEnvironment == .collapsed { accessories = [.disclosureIndicator()] } else { accessories = [] } }
-
6:11 - Customize the minimum, maximum, and preferred column widths
// Customize the minimum, maximum, and preferred column widths let splitViewController = // ... splitViewController.minimumPrimaryColumnWidth = 200.0 splitViewController.maximumPrimaryColumnWidth = 400.0 splitViewController.preferredSupplementaryColumnWidth = 500.0
-
7:37 - Show an inspector column
// Show an inspector column let splitViewController = // ... splitViewController.setViewController(inspectorViewController, for: .inspector) splitViewController.show(.inspector)
-
9:19 - Managing tab groups
// Managing tab groups let group = UITabGroup(title: "Library", ...) group.managingNavigationController = UINavigationController() // ... // MARK: - UITabBarControllerDelegate func tabBarController( _ tabBarController: UITabBarController, displayedViewControllersFor tab: UITab, proposedViewControllers: [UIViewController]) -> [UIViewController] { if tab.identifier == "Library" && !self.allowsSelectingLibraryTab { return [] } else { return proposedViewControllers } }
-
10:25 - Preferred minimum size
// Specify a preferred minimum size class SceneDelegate: UIResponder, UIWindowSceneDelegate { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { let windowScene = scene as! UIWindowScene windowScene.sizeRestrictions?.minimumSize.width = 500.0 } }
-
11:57 - Position content using the layout margins guide
// Position content using the layout margins guide let containerView = // ... let contentView = // ... let contentGuide = containerView.layoutMarginsGuide NSLayoutConstraint.activate([ contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor), contentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor), contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor) contentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor) ])
-
12:34 - Specify the window control style
// Specify the window control style class SceneDelegate: UIResponder, UIWindowSceneDelegate { func preferredWindowingControlStyle( for scene: UIWindowScene) -> UIWindowScene.WindowingControlStyle { return .unified } }
-
13:04 - Respect the window control area
// Respect the window control area let containerView = // ... let contentView = // ... let contentGuide = containerView.layoutGuide(for: .margins(cornerAdaptation: .horizontal) NSLayoutConstraint.activate([ contentView.topAnchor.constraint(equalTo: contentGuide.topAnchor), contentView.leadingAnchor.constraint(equalTo: contentGuide.leadingAnchor), contentView.bottomAnchor.constraint(equalTo: contentGuide.bottomAnchor), contentView.trailingAnchor.constraint(equalTo: contentGuide.trailingAnchor) ])
-
13:57 - Request orientation lock
// Request orientation lock class RaceViewController: UIViewController { override var prefersInterfaceOrientationLocked: Bool { return isDriving } // ... var isDriving: Bool = false { didSet { if isDriving != oldValue { setNeedsUpdateOfPrefersInterfaceOrientationLocked() } } } }
-
14:18 - Observe the interface orientation lock
// Observe the interface orientation lock class SceneDelegate: UIResponder, UIWindowSceneDelegate { var game = Game() func windowScene( _ windowScene: UIWindowScene, didUpdateEffectiveGeometry previousGeometry: UIWindowScene.Geometry) { let wasLocked = previousGeometry.isInterfaceOrientationLocked let isLocked = windowScene.effectiveGeometry.isInterfaceOrientationLocked if wasLocked != isLocked { game.pauseIfNeeded(isInterfaceOrientationLocked: isLocked) } } }
-
14:44 - Query whether the scene is resizing
// Query whether the scene is resizing class SceneDelegate: UIResponder, UIWindowSceneDelegate { var gameAssetManager = GameAssetManager() var previousSceneSize = CGSize.zero func windowScene( _ windowScene: UIWindowScene, didUpdateEffectiveGeometry previousGeometry: UIWindowScene.Geometry) { let geometry = windowScene.effectiveGeometry let sceneSize = geometry.coordinateSpace.bounds.size if !geometry.isInteractivelyResizing && sceneSize != previousSceneSize { previousSceneSize = sceneSize gameAssetManager.updateAssets(sceneSize: sceneSize) } } }
-
-
- 0:00 - Introduction
Make UIKit apps flexible and adaptable across different screen sizes and platforms using scenes, container view controllers, and other APIs.
- 0:58 - Scenes
Scenes represent distinct instances of an app's UI. Each Scene independently manages its state, and seamlessly restores upon reconnection. Scenes provide context about the app's display, such as screen size and window geometry. Starting with the next major release following iOS 26, adopting the UIScene life cycle will be mandatory.
- 4:58 - Container view controllers
Container view controllers like 'UISplitViewController' and 'UITabBarController' manage the layout of one or more child view controllers. They help make apps flexible, adaptable, and customizable. Use the UISceneRestrictions API to express the minimum size for scenes in the app.
- 10:45 - Adaptivity
Layout guides and margins help position an app’s content consistently within the device’s safe area. iPadOS 26 introduces a new window control. Apps can specify 'preferredWindowingControlStyle' and layout guides to accommodate these controls. Adaptive UIs need to respond quickly to resizing and orientation changes, but certain apps may want to override 'prefersInterfaceOrientationLocked' to temporarily lock the orientation. For computationally expensive operations, check 'isInteractivelyResizing' to perform the operations after an interaction finishes. 'UIRequiresFullscreen' is deprecated.
- 15:39 - Future compatibility
With iOS 26 SDK, apps automatically adapt to new screen sizes without needing manual updates or resubmission.
- 16:07 - Next steps
To build a flexible app, adopt scene life cycle, use container view controllers, and leverage APIs like layout guides.