Effective Geometry: Layout Without UIScreen.main


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.size

That 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.bounds

The 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.isInteractivelyResizing

coordinateSpace.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.systemFrame

Using 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.displayScale

This 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.bounds for individual views and view controllers.
  • Use effectiveGeometry for scene-level geometry.
  • Use the scene’s screen only when physical display information is required.
  • Avoid UIScreen.main for 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.