Highlight
iOS 27 requires all UIKit apps to stop hard-coding device type and screen orientation, and instead respond to arbitrary window sizes with Trait Collection and Size Classes. iPhone apps become fully resizable windows on iPad and Mac, and apps that have not migrated to the UIScene lifecycle will not launch.
Core Ideas
From “fixed device” to “any window”
In older UIKit apps, many people wrote layout decisions like this:
if UIDevice.current.userInterfaceIdiom == .pad {
// iPad layout
} else {
// iPhone layout
}
Or they chose interface structure based on screen orientation. This logic breaks completely on iOS 27.
iPhone apps can now be freely resized to any aspect ratio in iPhone Mirroring on Mac. iPhone-only apps running on iPad are no longer fixed-ratio “large phones”; they are fully resizable like other iPad apps. This means your app may run in a 16:9 Mac window or on a tall iPhone screen, while userInterfaceIdiom remains .phone.
Apple’s recommendation is direct: stop using device type for layout decisions, and use Size Classes instead.
The UIScene lifecycle is now mandatory
(02:22)
Apps built with the latest SDK will fail to launch if they do not adopt the UIScene lifecycle. This is the infrastructure for adaptive layouts. Without Scenes, the system cannot manage multiple window sizes and lifecycle states.
If you are still using the old AppDelegate model, migrate as soon as possible. Apple points to the WWDC25 session “Make your UIKit app more flexible” and the documentation “Transitioning to the UIKit scene-based life cycle” as references.
Say goodbye to UIScreen.main
(03:02)
When an app is mirrored to Mac or moved to an external display on iPad, the screen associated with its Scene can change. UIScreen.main will always return the wrong screen information.
The correct approach is to fetch it dynamically from the window scene:
let screen = window?.windowScene?.screen
An even better approach is to remove screen dependencies entirely and use replacement APIs in Trait Collection.
The correct way to get display scale and available space
(03:49)
Instead of using UIScreen.main.scale to get display scale, use traitCollection.displayScale:
override func layoutSubviews() {
super.layoutSubviews()
let displayScale = traitCollection.displayScale
// Adjust drawing logic based on displayScale
}
UIKit automatically tracks trait reads in common methods such as layoutSubviews and draw(_:). When displayScale changes, the system calls those methods again automatically, without manual observation.
For places that automatic tracking cannot cover, use registerForTraitChanges:
let displayScaleTrait: [UITrait] = [UITraitDisplayScale.self]
registerForTraitChanges(displayScaleTrait) {
(view: GalleryView, previousTraitCollection: UITraitCollection) in
view.cache.invalidate()
}
(04:36)
Available space should no longer come from UIScreen.main.bounds. Instead, read the Scene’s effective geometry or the view’s own bounds:
// Observe geometry changes in SceneDelegate
func windowScene(
_ windowScene: UIWindowScene,
didUpdateEffectiveGeometry previousEffectiveGeometry: UIWindowScene.Geometry
) {
let geometry = windowScene.effectiveGeometry
let availableSpace = geometry.coordinateSpace.bounds
}
// Read the view's bounds directly in ViewController
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
let availableSpace = view.bounds.size
}
New capabilities for Tab Bar and Navigation Bar
(09:51)
iPhone apps can now choose to show a sidebar instead of a bottom Tab Bar in large windows:
tabBarController.sidebar.preferredPlacement = .sidebar
The system decides whether to show the sidebar based on the horizontal Size Class. You can check tabBarController.sidebar.isAvailable to determine whether the current environment supports it.
You can also set a prominent tab that is always visible:
tabBarController.prominentTabIdentifier = "cart"
(10:53)
Navigation Bar adds control over scroll minimization:
navigationItem.barMinimizationBehavior = .always
navigationItem.barMinimizationSafeAreaAdjustment = .never
(11:30)
Body protocols in CoreMotion and CoreLocation
(08:12)
UIView now conforms to the Body protocols in CoreMotion and CoreLocation. Assign the View directly to the motion manager, and the system automatically handles coordinate-space conversion:
override func viewDidLoad() {
super.viewDidLoad()
motionManager.deviceMotionBody = view
locationManager.headingBody = view
}
You no longer need to manually calculate coordinate rotation based on screen orientation.
Apple Intelligence integration
(12:56)
Menus in iOS 27 automatically show an Ask Siri button. You can use the new View Annotations API to annotate specific Views with AppEntity, giving Siri more precise context.
If your app supports drag and drop, Siri can load resources directly from your drag delegate. Note that drag may be triggered by Siri without a gesture. Do not present modal UI in sessionWillBegin; put stateful UI initialization code in sessionDidMove.
Xcode 27’s App Modernization Skill
(14:16)
Xcode 27 includes an App Modernization skill that can automate most migration work:
- Replace
UIScreen.maincalls withtraitCollectionor scene bounds checks - Replace device type checks with Size Class checks
- Convert the app lifecycle to the Scene lifecycle
- Ask clarifying questions for complex tasks and add comments that mark remaining work for large tasks
Export the skill for use in other tools:
xcrun agent skills export
(15:05)
Details
Code example 1: get screen information from the window scene
When an app is mirrored to Mac or moved to an external display on iPad, the screen associated with its Scene can change. UIScreen.main will always return the wrong screen information.
The correct approach is to fetch it dynamically from the window scene:
let screen = window?.windowScene?.screen
Key point: UIScreen.main is deprecated on iOS 27 because it cannot handle multi-screen scenarios. windowScene.screen always returns the screen where the current Scene actually resides, and it updates automatically when switching between iPhone Mirroring and an external display.
Code example 2: observe trait changes with registerForTraitChanges
UIKit automatically tracks trait reads in methods such as layoutSubviews and draw(_:), then redraws automatically when displayScale changes. But for places that automatic tracking cannot cover, such as cache invalidation or background data recomputation, you need to register explicitly:
let displayScaleTrait: [UITrait] = [UITraitDisplayScale.self]
registerForTraitChanges(displayScaleTrait) {
(view: GalleryView, previousTraitCollection: UITraitCollection) in
view.cache.invalidate()
}
You can also observe combinations of multiple traits:
let geometryTraits: [UITrait] = [
UITraitHorizontalSizeClass.self,
UITraitVerticalSizeClass.self
]
registerForTraitChanges(geometryTraits) { (vc: MyViewController, _) in
vc.updateLayoutForCurrentSizeClass()
}
Key point: The closure parameter type for registerForTraitChanges must match the actual type of the observed object, such as GalleryView or MyViewController. The system uses type inference to dispatch callbacks. The closure is called synchronously when traits change, so it is best for lightweight state updates. If you need asynchronous or complex logic, trigger your own state machine from the closure.
Code example 3: Tab Bar sidebar adaptation
iPhone apps can automatically switch to sidebar navigation in large windows, saving bottom space:
tabBarController.sidebar.preferredPlacement = .sidebar
The system decides whether to show the sidebar based on the horizontal Size Class. You can check tabBarController.sidebar.isAvailable to determine whether the current environment supports it.
You can also set a prominent tab that is always visible, ensuring a core feature remains reachable when the sidebar collapses:
tabBarController.prominentTabIdentifier = "cart"
Key point: preferredPlacement = .sidebar tells the system to prefer the sidebar when conditions allow. When the window shrinks to a compact horizontal Size Class, the system automatically switches back to the bottom Tab Bar without manual handling. The tab matching prominentTabIdentifier is highlighted in sidebar mode and kept as a floating button in collapsed mode.
Key API migrations for adaptive layout
| Old approach | New approach | Notes |
|---|---|---|
UIScreen.main | window?.windowScene?.screen | Fetch dynamically from the window scene |
UIScreen.main.scale | traitCollection.displayScale | Automatically tracked by Trait Collection |
UIScreen.main.bounds | view.bounds.size or windowScene.effectiveGeometry | Read the actual space of the View or Scene |
UIDevice.current.userInterfaceIdiom | traitCollection.horizontalSizeClass | Make layout decisions with Size Class |
UIApplication.shared.statusBarOrientation | traitCollection.horizontalSizeClass / verticalSizeClass | Orientation checks are no longer meaningful |
Special handling for game apps
Games often need a fixed orientation. iOS 27 adjusts the behavior of UIRequiresFullscreen: it no longer fully disables resizing, but enables discrete resizing. Every time the window size changes, the system switches to a screen configuration that matches the new size, and the game continues rendering at full quality.
Testing recommendations
Device Hub and Xcode Previews in Xcode 27 support “enter resizing mode,” so you can freely drag device edges to resize without switching between multiple simulators. After initial validation, make sure to test iPhone Mirroring and iPad external display scenarios on real devices.
Key Takeaways
-
Replace
UIScreen.mainacross old projectsWhat to do: Search for all
UIScreen.mainreferences in the project and replace them withtraitCollection.displayScaleor window scene references.Why it is worth doing: On iOS 27,
UIScreen.mainreturns incorrect data, which can cause blurry images and misplaced layouts.How to start: Use Xcode’s Find Navigator to search for
UIScreen.mainand replace each use. Or let Xcode 27’s App Modernization skill handle it automatically. -
Add sidebar adaptation to Tab Bar
What to do: Set
tabBarController?.sidebar.preferredPlacement = .sidebarinviewDidLoad.Why it is worth doing: When an iPhone app is resized into a large Mac window, the bottom Tab Bar wastes a lot of horizontal space. A sidebar can show more navigation hierarchy.
How to start: Enable it with one line of code, then use
sidebar.isAvailableto check whether the sidebar is currently shown and adjust related UI. -
Protect core navigation with a prominent tab
What to do: Set
prominentTabIdentifierfor the most important tab, such as cart or home.Why it is worth doing: The sidebar may collapse when the window becomes smaller. A prominent tab keeps the core feature always reachable.
How to start: After creating
UITabBarController, settabBarController.prominentTabIdentifier = "your-tab-id". -
Let Navigation Bar collapse automatically during scrolling
What to do: Set
navigationItem.barMinimizationBehavior = .always.Why it is worth doing: The content area grows and the reading experience improves. The system only collapses the bar by default under certain conditions; forcing it on can make behavior consistent.
How to start: Set the property in ViewController initialization code. If you handle safe area yourself, also set
barMinimizationSafeAreaAdjustment = .never. -
Export the Xcode skill into your own workflow
What to do: Run
xcrun agent skills exportto export Xcode’s modernization skill as a markdown file.Why it is worth doing: You can reuse Apple’s official modernization rules in Cursor, Claude Code, and other tools, keeping the team’s migration strategy consistent.
How to start: Run the command in Terminal. The exported file can be imported directly into other AI coding tools.
Related Sessions
- Make your UIKit app more flexible — A deeper UIKit adaptive layout guide and detailed steps for migrating to the Scene lifecycle
- Elevate your tab and sidebar experience in iPadOS — Deep integration of Tab Bar and sidebar, plus best practices for managing Tab Groups
- Get the most out of Device Hub — A complete introduction to the new Device Hub in Xcode 27, including free resizing tests
- Explore advanced App Intents features for Siri and Apple Intelligence — Detailed usage of the View Annotations API and Siri integration
- SwiftUI — If you also maintain SwiftUI code, learn what is new for adaptive layout in SwiftUI
Comments
GitHub Issues · utterances