How to Lock Orientation for One Screen on iPad Without Full Screen

How to Lock Orientation for One Screen on iPad Without Full Screen

Written by

in

Quick answer: On iPad, rotation is a window resize, so per-screen orientation masks are ignored whenever multitasking (Split View, Slide Over, Stage Manager) is available. You can only force a lock by opting out of multitasking with UIRequiresFullScreen, which Apple deprecated in iPadOS 26. The durable fix is to stop treating it as a rotation problem and make the screen — usually a camera preview — survive any window size.

Why this happens

On iPad, rotation is not a special event — it is a window resize. When multitasking is available (Split View, Slide Over, Stage Manager), the user can change your window’s size at any moment, so the system stops honouring per-view-controller orientation preferences. supportedInterfaceOrientations and application(_:supportedInterfaceOrientationsFor:) are still called, but the returned mask no longer decides how the window is sized.

That is exactly why the Flutter documentation says SystemChrome.setPreferredOrientations is only respected on iPad when multitasking is disabled, and why setting UIRequiresFullScreen in the Xcode target made the lock start working: it opts the app out of multitasking and brings the old orientation model back. Apple has deprecated UIRequiresFullScreen as of iPadOS 26, so that switch is a stopgap, not a design.

The second half of the problem is the scanner itself. A camera preview that “becomes misaligned after rotating” is usually a preview layer whose frame or rotation angle was never updated after the size change. Locking the orientation hides that bug on Android; on iPad it only postpones it, because the window can still be resized by the user or by the system.

How to fix it

1. Check what the app currently declares

Open the target’s Info tab and look at two keys:

  • UISupportedInterfaceOrientations~ipad — the orientations the app claims to support.
  • UIRequiresFullScreen — if YES, Split View and Slide Over are disabled and per-screen locks work again.

If UIRequiresFullScreen is NO (or missing), a per-screen mask will still work on iPhone and will be ignored on iPad. Confirm this before changing any code: run the app on an iPad, drag it into Slide Over, and watch whether the scanner screen rotates anyway.

2. Keep the per-screen mask for iPhone

A single shared mask plus a geometry request is the cleanest way to express “this screen is portrait” without touching the rest of the app.

import UIKit

@main
final class AppDelegate: UIResponder, UIApplicationDelegate {
    static var orientationLock: UIInterfaceOrientationMask = .all

    func application(_ application: UIApplication,
                     supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        AppDelegate.orientationLock
    }
}

Then, in the view controller that hosts the scanner (or in a UIViewControllerRepresentable wrapper around your SwiftUI screen):

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    AppDelegate.orientationLock = .portrait
    setNeedsUpdateOfSupportedInterfaceOrientations()

    if let scene = view.window?.windowScene {
        scene.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait)) { error in
            print("Geometry update failed: \(error)")
        }
    }
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    AppDelegate.orientationLock = .all
    setNeedsUpdateOfSupportedInterfaceOrientations()
}

On iPhone this pins the screen to portrait and restores everything on the way out. On iPad the request is a hint: the window may still be resized, so treat this as a nicety, not a guarantee.

3. Make the camera preview survive a resize

This is the fix that actually matters. An AVCaptureVideoPreviewLayer does not resize itself, and its connection keeps whatever rotation angle it was given. Update both whenever the layout changes.

import AVFoundation
import UIKit

final class ScannerPreviewView: UIView {
    override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }

    private var previewLayer: AVCaptureVideoPreviewLayer {
        layer as! AVCaptureVideoPreviewLayer
    }

    var session: AVCaptureSession? {
        get { previewLayer.session }
        set { previewLayer.session = newValue }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        previewLayer.videoGravity = .resizeAspectFill
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        previewLayer.frame = bounds
        updateRotation()
    }

    private func updateRotation() {
        guard let connection = previewLayer.connection else { return }
        let angle = rotationAngle(for: window?.windowScene?.interfaceOrientation ?? .portrait)
        guard connection.isVideoRotationAngleSupported(angle) else { return }
        connection.videoRotationAngle = angle
    }

    private func rotationAngle(for orientation: UIInterfaceOrientation) -> CGFloat {
        // Back camera mapping. A front camera needs the mirrored values.
        switch orientation {
        case .portrait: return 90
        case .portraitUpsideDown: return 270
        case .landscapeLeft: return 180
        case .landscapeRight: return 0
        default: return 90
        }
    }
}

videoRotationAngle replaced the older videoOrientation property, which is deprecated. If you are still setting videoOrientation, that alone can explain a preview that looks correct until the second rotation.

4. Pin the scanner to a fixed aspect ratio

If the preview must never change shape, give it a container with a fixed aspect ratio and let the rest of the screen letterbox around it. In SwiftUI:

struct ScannerScreen: View {
    var body: some View {
        GeometryReader { proxy in
            let side = min(proxy.size.width, proxy.size.height)
            ScannerView()
                .frame(width: side, height: side)
                .clipped()
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
}

The preview layer now sees one size for its whole lifetime, so a window resize cannot misalign it.

5. Rebuild the screen when the size changes

When a view genuinely cannot tolerate resizing — a third-party scanner surface, for example — tear it down and build it again on the size change. In SwiftUI, an identity change is enough:

struct ScannerScreen: View {
    @State private var sessionID = UUID()

    var body: some View {
        GeometryReader { proxy in
            ScannerView()
                .id(sessionID)
                .onChange(of: proxy.size) { _, _ in
                    sessionID = UUID()
                }
        }
    }
}

In UIKit the equivalent hook is viewWillTransition(to:with:). Restarting a capture session is expensive, so debounce the change or only rebuild when the aspect ratio actually flips.

6. If you truly need a hard lock, accept full screen

Setting UIRequiresFullScreen to YES restores the iPhone-style behaviour on iPad: the mask is honoured, the scanner stays portrait, and Split View and Slide Over disappear for the whole app. That is a product decision, not a technical one, and it is on borrowed time because the key is deprecated in iPadOS 26. If you take this route, plan the migration to resize handling now rather than after the key stops working.

How to prevent it

  • Treat every iPad screen as resizable by default; test in Split View and Slide Over, not just in the simulator’s portrait and landscape.
  • Never read UIDevice.current.orientation to decide layout on iPad — derive it from the window scene or from the actual view size.
  • Update AVCaptureVideoPreviewLayer frame and videoRotationAngle in layoutSubviews, not once in viewDidLoad.
  • Keep the orientation mask in one place (the app delegate) so a single screen cannot leak its preference into the rest of the app.
  • If you support Flutter or another cross-platform layer, assume setPreferredOrientations is a hint on iPad and handle resizing in the native view.

FAQ

Why is supportedInterfaceOrientations ignored on iPad?

Because the app allows multitasking. When Split View, Slide Over or Stage Manager can resize your window at any time, the system cannot let a single view controller dictate the window’s shape, so the mask is only consulted on iPhone or when UIRequiresFullScreen is set.

Does UIRequiresFullScreen still work in iPadOS 26?

It still disables multitasking and restores orientation locking, but Apple has deprecated the key, so it should be treated as a temporary workaround. Build the resize-tolerant version of the screen while the key still works.

Can I lock orientation in Flutter on iPad?

Not per screen, for the same reason: SystemChrome.setPreferredOrientations maps to the same orientation mask that iPad ignores while multitasking is enabled. The practical answer is to keep the scanner’s native preview view correctly sized and rotated, and let the rest of the app rotate freely.

Sources & credits

Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.