WWDC Quick Look đź’“ By SwiftGGTeam
Modernize your UIKit app

Modernize your UIKit app

Watch original video

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
}

(05:19) (05:35)

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.main calls with traitCollection or 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 approachNew approachNotes
UIScreen.mainwindow?.windowScene?.screenFetch dynamically from the window scene
UIScreen.main.scaletraitCollection.displayScaleAutomatically tracked by Trait Collection
UIScreen.main.boundsview.bounds.size or windowScene.effectiveGeometryRead the actual space of the View or Scene
UIDevice.current.userInterfaceIdiomtraitCollection.horizontalSizeClassMake layout decisions with Size Class
UIApplication.shared.statusBarOrientationtraitCollection.horizontalSizeClass / verticalSizeClassOrientation 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.main across old projects

    What to do: Search for all UIScreen.main references in the project and replace them with traitCollection.displayScale or window scene references.

    Why it is worth doing: On iOS 27, UIScreen.main returns incorrect data, which can cause blurry images and misplaced layouts.

    How to start: Use Xcode’s Find Navigator to search for UIScreen.main and 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 = .sidebar in viewDidLoad.

    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.isAvailable to check whether the sidebar is currently shown and adjust related UI.

  • Protect core navigation with a prominent tab

    What to do: Set prominentTabIdentifier for 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, set tabBarController.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 export to 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.

Comments

GitHub Issues · utterances