Skip to content

Fix single-scene assumption in updateSupportedInterfaceOrientations() #3

Description

@ivan-magda

Problem

InterfaceOrientationManager.updateSupportedInterfaceOrientations() at InterfaceOrientationManager.swift:168-178 uses .first on UIApplication.shared.connectedScenes to find the window scene:

private func updateSupportedInterfaceOrientations() {
    if #available(iOS 16.0, *) {
        guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else {
            return Self.logger.error("No active window scene found")
        }
        windowScene.keyWindow?.rootViewController?.setNeedsUpdateOfSupportedInterfaceOrientations()
    } else {
        UIViewController.attemptRotationToDeviceOrientation()
    }
}

This has three issues:

  1. Non-deterministic selection: connectedScenes is a Set<UIScene>, meaning .first has no guaranteed ordering. It may grab a background scene rather than the active one.
  2. iPad multi-window: In iPad Split View, both scenes are .foregroundActive. Only updating one scene leaves the other with stale orientation constraints.
  3. App launch timing: During app startup (called from init()), scenes may still be in .foregroundInactive state before transitioning to .foregroundActive.

Research Findings

How orientation resolution works

The system determines allowed orientations through an intersection:

  1. application(_:supportedInterfaceOrientationsFor:) is called per window (the window parameter identifies which window)
  2. The root VC's supportedInterfaceOrientations is queried
  3. The system intersects both results

setNeedsUpdateOfSupportedInterfaceOrientations() (iOS 16+) operates on a specific VC in a specific window in a specific scene. Each scene's root VC must be notified independently.

Orientation is device-wide, not per-scene

On iPad, all scenes share the same physical screen and rotate together. You cannot have one scene in portrait and another in landscape. Since orientation is a physical device property, the library's app-wide constraint model is fundamentally correct — but all scenes need to be notified.

Scene activation states

State Description
.foregroundActive In foreground, receiving events
.foregroundInactive In foreground, NOT receiving events (e.g., during transitions)
.background In background
.unattached Connected but not yet visible

In iPad Split View, both scenes are .foregroundActive, so filtering by that state still returns multiple results.

iOS 26 future direction

Apple announced in WWDC 2025 (Make your UIKit app more flexible):

  • UIRequiresFullScreen is deprecated and will be ignored in future releases
  • New per-VC API: prefersInterfaceOrientationLocked provides Apple's officially recommended orientation locking
  • Scene lifecycle adoption is transitioning from encouraged to mandatory

See also TN3192: Migrating from UIRequiresFullScreen.

Proposed Solution

Iterate all connected window scenes instead of picking one:

private func updateSupportedInterfaceOrientations() {
    if #available(iOS 16.0, *) {
        for case let windowScene as UIWindowScene in UIApplication.shared.connectedScenes {
            windowScene.keyWindow?.rootViewController?
                .setNeedsUpdateOfSupportedInterfaceOrientations()
        }
    } else {
        UIViewController.attemptRotationToDeviceOrientation()
    }
}

This handles:

  • App launch: Zero scenes is a no-op loop (no crash, no error log needed)
  • iPad Split View: Both scenes get updated
  • Single-scene iPhone: The loop runs once, same behavior as before
  • Non-determinism: Eliminated — we don't pick one from the Set

Alternative considered: Filter for .foregroundActive with fallback

guard let windowScene = UIApplication.shared.connectedScenes
    .first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene
    ?? UIApplication.shared.connectedScenes.first as? UIWindowScene
else { ... }

This was rejected because it still only updates ONE scene, missing the second scene in iPad Split View.

Future consideration: iOS 26 adoption

When raising the minimum target to iOS 26+, consider adopting prefersInterfaceOrientationLocked as Apple's officially recommended replacement for orientation locking patterns.

References

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions