2025 SwiftUI & UI Frameworks
WWDC25 · 17 min · SwiftUI & UI Frameworks
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.
Watch at developer.apple.com ↗Chapters
Code shown on screen · 15 snippets
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)
}
}
} 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
}
} Handle life cycle events
// Handle life cycle events
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
var timerModel = TimerModel()
// ...
func sceneDidEnterBackground(_ scene: UIScene) {
timerModel.pause()
}
} 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
}
} 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 = []
}
} 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 Show an inspector column
// Show an inspector column
let splitViewController = // ...
splitViewController.setViewController(inspectorViewController, for: .inspector)
splitViewController.show(.inspector) 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
}
} 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
}
} 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)
]) Specify the window control style
// Specify the window control style
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
func preferredWindowingControlStyle(
for scene: UIWindowScene) -> UIWindowScene.WindowingControlStyle {
return .unified
}
} 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)
]) Request orientation lock
// Request orientation lock
class RaceViewController: UIViewController {
override var prefersInterfaceOrientationLocked: Bool {
return isDriving
}
// ...
var isDriving: Bool = false {
didSet {
if isDriving != oldValue {
setNeedsUpdateOfPrefersInterfaceOrientationLocked()
}
}
}
} 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)
}
}
} 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)
}
}
} Resources
Related sessions
-
16 min -
26 min