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— ifYES, 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.orientationto decide layout on iPad — derive it from the window scene or from the actual view size. - Update
AVCaptureVideoPreviewLayerframe andvideoRotationAngleinlayoutSubviews, not once inviewDidLoad. - 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
setPreferredOrientationsis 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.
Parts of this guide are adapted from content licensed under CC BY-SA 4.0; this article is shared under the same license.
