Greetings, traveler!
Resizable windows make screen-based layout assumptions unreliable. An app may run in a partial window, move to another display, or change size while it is running. In those cases, UIScreen.main.bounds describes the physical screen, not the space available to the app.
UIKit provides UIWindowScene.effectiveGeometry for scene-level geometry and windowScene(_:didUpdateEffectiveGeometry:) for reacting to changes.
Why UIScreen.main is the wrong layout source
Code that reads the main screen size assumes the application occupies the entire display:
let size = UIScreen.main.bounds.sizeThat assumption breaks when the window:
- occupies only part of an iPad display
- runs through iPhone Mirroring
- moves to an external screen
- changes size interactively
- shares the screen with another app
The physical screen and the application window are separate concepts. Layout should respond to the space available to the current window.
Reading the effective scene geometry
effectiveGeometry belongs to UIWindowScene:
let geometry = windowScene.effectiveGeometry
let bounds = geometry.coordinateSpace.boundsThe returned UIWindowScene.Geometry describes the current geometry of the scene.
Its most relevant properties include:
let geometry = windowScene.effectiveGeometry
let bounds = geometry.coordinateSpace.bounds
let systemFrame = geometry.systemFrame
let orientation = geometry.interfaceOrientation
let isResizing = geometry.isInteractivelyResizingcoordinateSpace.bounds gives the size of the scene in its own coordinate space. This is usually the correct value for scene-level rendering or resource management.
systemFrame describes the scene’s position in the system coordinate space. It is useful when the location of the window matters, not only its size.
interfaceOrientation provides the current interface orientation. It replaces the deprecated UIWindowScene.interfaceOrientation property.
isInteractivelyResizing tells you whether the user is currently resizing the window.
Reacting to geometry changes
A scene delegate can implement windowScene(_:didUpdateEffectiveGeometry:):
func windowScene(
_ windowScene: UIWindowScene,
didUpdateEffectiveGeometry previousGeometry: UIWindowScene.Geometry
) {
let geometry = windowScene.effectiveGeometry
let size = geometry.coordinateSpace.bounds.size
canvas.updateViewport(for: size)
}The callback parameter contains the previous geometry. The current geometry must be read from windowScene.effectiveGeometry.
This detail matters when comparing the old and new values:
func windowScene(
_ windowScene: UIWindowScene,
didUpdateEffectiveGeometry previousGeometry: UIWindowScene.Geometry
) {
let currentGeometry = windowScene.effectiveGeometry
let previousSize = previousGeometry.coordinateSpace.bounds.size
let currentSize = currentGeometry.coordinateSpace.bounds.size
guard previousSize != currentSize else {
return
}
canvas.updateViewport(for: currentSize)
}The callback may also run when the scene moves to another screen, even if its logical size does not change. Compare only the properties relevant to your use case.
Handling interactive resizing
Lightweight layout updates can run during every geometry change. Expensive work should usually wait until interactive resizing finishes.
func windowScene(
_ windowScene: UIWindowScene,
didUpdateEffectiveGeometry previousGeometry: UIWindowScene.Geometry
) {
let geometry = windowScene.effectiveGeometry
let size = geometry.coordinateSpace.bounds.size
renderer.updateViewport(for: size)
guard !geometry.isInteractivelyResizing else {
return
}
renderer.rebuildResources(for: size)
}This pattern is useful for work such as:
- rebuilding large textures
- regenerating images
- recreating canvas resources
- recalculating expensive caches
The interface can remain responsive during resizing while resource-heavy work runs only after the final size is known.
Use view.bounds for view layout
effectiveGeometry describes the whole scene. It does not necessarily describe the space available to a particular view controller.
A view may occupy only part of the scene because it is inside a split view, sheet, inspector, or custom container. For local layout, use the nearest container:
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
updateLayout(for: view.bounds.size)
}A useful distinction is:
// Layout inside a view hierarchy
view.bounds
// Geometry of the complete scene
windowScene.effectiveGeometry.coordinateSpace.bounds
// Position of the scene in system coordinates
windowScene.effectiveGeometry.systemFrameUsing view.bounds also makes components easier to reuse in different presentation contexts.
Do not choose layout from the device type
A .phone interface idiom no longer guarantees a narrow window. An iPhone app can run inside a resizable environment while still reporting the phone idiom.
Avoid layout decisions based only on this check:
if traitCollection.userInterfaceIdiom == .phone {
useCompactLayout()
}Use size classes when they provide enough information:
if traitCollection.horizontalSizeClass == .regular {
useExpandedLayout()
} else {
useCompactLayout()
}For layouts with precise breakpoints, read the container width directly:
switch view.bounds.width {
case 700...:
useThreeColumnLayout()
case 480...:
useTwoColumnLayout()
default:
useSingleColumnLayout()
}The available space is a more reliable input than the device category.
Screen scale and physical display information
UIScreen still has valid uses. It describes a physical display rather than an application window.
When physical screen information is required, access the screen associated with the current scene:
guard let screen = view.window?.windowScene?.screen else {
return
}For rendering scale inside a view, prefer the trait collection:
let scale = traitCollection.displayScaleThis follows the current environment and updates when the view moves between displays with different characteristics.
API availability
UIWindowScene.effectiveGeometry is available from iOS 16.
New windowing behavior makes resizable iPhone applications more common, so code that assumes a fixed full-screen canvas is more likely to fail.
The migration rule is straightforward:
- Use
view.boundsfor individual views and view controllers. - Use
effectiveGeometryfor scene-level geometry. - Use the scene’s
screenonly when physical display information is required. - Avoid
UIScreen.mainfor layout decisions.
Resizable windows do not require a completely new layout architecture. They require layout code to depend on its actual container instead of the dimensions of the device.
