From 18bfbce54b3404df44f3883e3c326221746a4ca9 Mon Sep 17 00:00:00 2001 From: Andre Destro Date: Tue, 27 Oct 2026 10:28:47 +0000 Subject: [PATCH 1/2] fix: rotate the scanner to the forced scanOrientation in UIScene apps --- .../OSBARCCaptureOutputDecoder.swift | 56 +++++++++++++++-- .../OSBARCCaptureSessionManager.swift | 6 +- .../Extensions/UIApplication+Window.swift | 14 +++++ ...eOrientationMask+OSBARCModelMappable.swift | 27 ++++++++ .../Scanner/OSBARCScannerViewController.swift | 44 ++++++++++++- .../OSBARCScannerViewHostingController.swift | 61 +++++++++++++++++++ .../OSBARCManagerTests.swift | 26 ++++++++ 7 files changed, 224 insertions(+), 10 deletions(-) diff --git a/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureOutputDecoder.swift b/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureOutputDecoder.swift index 4cca08c..f375ec1 100644 --- a/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureOutputDecoder.swift +++ b/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureOutputDecoder.swift @@ -14,6 +14,12 @@ final class OSBARCCaptureOutputDecoder: NSObject, AVCaptureVideoDataOutputSample /// A hint, to scan a specific format (e.g. only qr code). `Nil` or `unknown` value means it can scan all. private var hint: OSBARCScannerHint? + /// Orientation of the camera preview. + /// Frames passed to Vision follow it, so that they match what's displayed on screen. + private var videoOrientation: AVCaptureVideoOrientation? + /// Last on-screen scan frame, kept to recompute the region of interest when the orientation changes. + private var scanFrame: CGRect? + /// The publisher's cancellable instance collector. private var cancellables: Set = [] @@ -34,8 +40,9 @@ final class OSBARCCaptureOutputDecoder: NSObject, AVCaptureVideoDataOutputSample .publisher(for: .scanFrameChanged) .receive(on: RunLoop.main) // receive this on the main thread .sink { // performed this when `scanFrameChanged` gets triggered (on screen rotation). - if let imageRect = $0.object as? CGRect, let regionOfInterest = self.scanRegionOfInterest(for: imageRect) { - self.detectBarcodeRequest.regionOfInterest = regionOfInterest // update `regionOfInterest` + if let imageRect = $0.object as? CGRect { + self.scanFrame = imageRect + self.updateRegionOfInterest() } } .store(in: &cancellables) @@ -67,7 +74,14 @@ final class OSBARCCaptureOutputDecoder: NSObject, AVCaptureVideoDataOutputSample ) try? imageRequestHandler.perform([self.detectBarcodeRequest]) } - + + /// Keeps the decoding aligned with the camera preview. Must be called on the main thread. + /// - Parameter videoOrientation: The orientation the camera preview is now displayed in. + func update(videoOrientation: AVCaptureVideoOrientation) { + self.videoOrientation = videoOrientation + self.updateRegionOfInterest() + } + /// Vision request to perform. It gets initialised only once and when needed. lazy private var detectBarcodeRequest: VNDetectBarcodesRequest = { let barcodeRequest = VNDetectBarcodesRequest(completionHandler: { request, error in @@ -100,13 +114,28 @@ private extension OSBARCCaptureOutputDecoder { /// - Parameter imageRect: The coordinates to convert /// - Returns: The converted coordinates. If can return `nil` if not able to fetch the screens bounds to base on. func scanRegionOfInterest(for imageRect: CGRect) -> CGRect? { - guard let screenBounds = UIApplication.firstKeyWindowForConnectedScenes?.windowScene?.screen.bounds else { return nil } - return VNNormalizedRectForImageRect(imageRect, Int(screenBounds.width), Int(screenBounds.height)) + // The window's bounds follow the layout the scan frame comes from; the screen's lag behind a forced rotation. + guard let windowBounds = UIApplication.firstKeyWindowForConnectedScenes?.bounds else { return nil } + return VNNormalizedRectForImageRect(imageRect, Int(windowBounds.width), Int(windowBounds.height)) } - + + /// Updates the Vision request's region of interest from the last scan frame. + func updateRegionOfInterest() { + if let scanFrame = self.scanFrame, let regionOfInterest = self.scanRegionOfInterest(for: scanFrame) { + self.detectBarcodeRequest.regionOfInterest = regionOfInterest + } + } + /// Converts device orientation into the intended image's display orientation. /// - Returns: The equivalent `CGImagePropertyOrientation` to use for the device's current orientation. func deviceExifOrientation() -> CGImagePropertyOrientation { + // The region of interest comes from the on-screen scan frame, so the image has to follow the preview's + // orientation, which differs from the device's when the scanner forces portrait or landscape. + if let videoOrientation = self.videoOrientation, + let orientation = CGImagePropertyOrientation(videoOrientation: videoOrientation) { + return orientation + } + switch UIDevice.current.orientation { case .landscapeRight: return .upMirrored case .portraitUpsideDown: return .leftMirrored @@ -123,3 +152,18 @@ private extension OSBARCCaptureOutputDecoder { } } } + +extension CGImagePropertyOrientation { + /// Maps the camera preview's orientation into the orientation of the frames passed to Vision. + /// - Parameter videoOrientation: Value to be converted from. Returns `nil` for unknown values. + init?(videoOrientation: AVCaptureVideoOrientation) { + switch videoOrientation { + case .portrait: self = .rightMirrored + case .portraitUpsideDown: self = .leftMirrored + // `AVCaptureVideoOrientation.landscapeLeft` is `UIDeviceOrientation.landscapeRight`. + case .landscapeLeft: self = .upMirrored + case .landscapeRight: self = .downMirrored + @unknown default: return nil + } + } +} diff --git a/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureSessionManager.swift b/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureSessionManager.swift index 327069e..6e27b76 100644 --- a/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureSessionManager.swift +++ b/Sources/OSBarcodeLib/Scanner/CameraManager/OSBARCCaptureSessionManager.swift @@ -92,7 +92,7 @@ final class OSBARCCaptureSessionManager: OSBARCCameraManager { Subsequent orientation changes are handled by `viewWillTransition(to:with:)`. */ if videoPreviewLayer.connection?.isVideoOrientationSupported == true, - let interfaceOrientation = UIApplication.firstKeyWindowForConnectedScenes?.windowScene?.interfaceOrientation, + let interfaceOrientation = UIApplication.currentInterfaceOrientation, var initialVideoOrientation = AVCaptureVideoOrientation(interfaceOrientation: interfaceOrientation) { /* If the orientation has to be portrait but the device is not on that mode, orientation is set to `portrait`. @@ -106,6 +106,7 @@ final class OSBARCCaptureSessionManager: OSBARCCameraManager { } videoPreviewLayer.connection?.videoOrientation = initialVideoOrientation + self.outputDecoder.update(videoOrientation: initialVideoOrientation) } } } @@ -143,9 +144,10 @@ final class OSBARCCaptureSessionManager: OSBARCCameraManager { videoPreviewLayer.frame = CGRect(x: 0, y: 0, width: rotationChange.value.width, height: rotationChange.value.height) if videoPreviewLayer.connection?.isVideoOrientationSupported == true, - let interfaceOrientation = UIApplication.firstKeyWindowForConnectedScenes?.windowScene?.interfaceOrientation, + let interfaceOrientation = UIApplication.currentInterfaceOrientation, let newVideoOrientation = AVCaptureVideoOrientation(interfaceOrientation: interfaceOrientation) { videoPreviewLayer.connection?.videoOrientation = newVideoOrientation + self.outputDecoder.update(videoOrientation: newVideoOrientation) } } } diff --git a/Sources/OSBarcodeLib/Scanner/Extensions/UIApplication+Window.swift b/Sources/OSBarcodeLib/Scanner/Extensions/UIApplication+Window.swift index 6fb614a..2801c59 100644 --- a/Sources/OSBarcodeLib/Scanner/Extensions/UIApplication+Window.swift +++ b/Sources/OSBarcodeLib/Scanner/Extensions/UIApplication+Window.swift @@ -9,4 +9,18 @@ extension UIApplication { .flatMap { ($0 as? UIWindowScene)?.windows ?? [] } .last { $0.isKeyWindow } } + + /// The current interface orientation of the key window's scene. + static var currentInterfaceOrientation: UIInterfaceOrientation? { + self.firstKeyWindowForConnectedScenes?.windowScene?.currentInterfaceOrientation + } +} + +extension UIWindowScene { + /// The scene's current interface orientation, read from `effectiveGeometry` on iOS 16+ + /// (`interfaceOrientation` is deprecated as of iOS 26). + var currentInterfaceOrientation: UIInterfaceOrientation { + if #available(iOS 16.0, *) { return self.effectiveGeometry.interfaceOrientation } + return self.interfaceOrientation + } } diff --git a/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift b/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift index 8a16d2c..161a405 100644 --- a/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift +++ b/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift @@ -10,4 +10,31 @@ extension UIInterfaceOrientationMask: OSBARCModelMappable { default: return Self.deviceTypeModel == .iphone ? .allButUpsideDown : .all } } + + /// Mask containing only the given interface orientation (`all` when it's unknown). + /// - Parameter interfaceOrientation: Value to be converted from. + init(interfaceOrientation: UIInterfaceOrientation) { + switch interfaceOrientation { + case .portrait: self = .portrait + case .portraitUpsideDown: self = .portraitUpsideDown + case .landscapeLeft: self = .landscapeLeft + case .landscapeRight: self = .landscapeRight + default: self = .all + } + } +} + +extension UIInterfaceOrientation { + /// Interface orientation matching the device's physical orientation. + /// Returns `nil` when the device is flat or the orientation is unknown. + /// - Parameter deviceOrientation: Value to be converted from. + init?(deviceOrientation: UIDeviceOrientation) { + switch deviceOrientation { + case .portrait: self = .portrait + case .portraitUpsideDown: self = .portraitUpsideDown + case .landscapeLeft: self = .landscapeRight // the landscape cases are mirrored between device and interface + case .landscapeRight: self = .landscapeLeft + default: return nil + } + } } diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewController.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewController.swift index 34c5a12..dc831bf 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewController.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewController.swift @@ -33,14 +33,54 @@ final class OSBARCScannerViewController: UIViewController { override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) - + + self.geometryObservation?.invalidate() + self.geometryObservation = nil + self.cameraManager.stop() } + /// Observes the window scene's geometry, so the preview follows rotations + /// that don't trigger a transition or a new layout. + private var geometryObservation: NSKeyValueObservation? + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + if #available(iOS 16.0, *), let windowScene = self.view.window?.windowScene { + self.geometryObservation = windowScene.observe(\.effectiveGeometry) { [weak self] _, _ in + DispatchQueue.main.async { self?.syncPreviewWithView() } + } + } + self.syncPreviewWithView() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Keeps the preview in sync with the view's size, including rotations that don't go through + // `viewWillTransition(to:with:)`, such as the one requested when the scanner is presented + // with a forced orientation. + self.syncPreviewWithView() + } + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { super.viewWillTransition(to: size, with: coordinator) - + let rotationToPerform: OSBARCCameraRotationChange = .init(value: (Int(size.height), Int(size.width))) try? self.cameraManager.apply(change: rotationToPerform) + + // The interface orientation only changes once the transition ends, + // and a 180° rotation doesn't trigger a new layout. + coordinator.animate(alongsideTransition: nil) { [weak self] _ in self?.syncPreviewWithView() } + } + + /// Resizes the camera preview to the view's bounds and aligns it with the current interface orientation. + private func syncPreviewWithView() { + let size = self.view.bounds.size + CATransaction.begin() + CATransaction.setDisableActions(true) + try? self.cameraManager.apply(change: OSBARCCameraRotationChange(value: (Int(size.height), Int(size.width)))) + CATransaction.commit() } } diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift index d8ec730..1b1fd52 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift @@ -20,4 +20,65 @@ class OSBARCScannerViewHostingController: UIHostingController } override var supportedInterfaceOrientations: UIInterfaceOrientationMask { .map(self.orientationModel) } + + /// Window scene the scanner is presented in, used to restore its orientation on dismissal. + private weak var presentingWindowScene: UIWindowScene? + /// Interface orientation in place before the scanner forced its own. + private var previousInterfaceOrientation: UIInterfaceOrientation? + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + // With the UIScene lifecycle, presenting a full-screen controller with narrower + // `supportedInterfaceOrientations` doesn't rotate the interface by itself, + // so a forced orientation has to be requested explicitly. + guard self.orientationModel != .adaptive, + let windowScene = self.view.window?.windowScene + ?? UIApplication.firstKeyWindowForConnectedScenes?.windowScene + else { return } + self.presentingWindowScene = windowScene + self.previousInterfaceOrientation = windowScene.currentInterfaceOrientation + Self.requestInterfaceOrientations(self.supportedInterfaceOrientations, in: windowScene, updating: self) + } + + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) + + guard let windowScene = self.presentingWindowScene, + let previousInterfaceOrientation = self.previousInterfaceOrientation + else { return } + // Follow how the device is held now, if the app supports it. + // Otherwise, go back to the orientation in place before scanning. + let rootViewController = windowScene.keyWindow?.rootViewController + let supportedOrientations = rootViewController?.supportedInterfaceOrientations ?? .all + let deviceOrientation = UIInterfaceOrientation(deviceOrientation: UIDevice.current.orientation) + let targetOrientation = deviceOrientation.flatMap { + supportedOrientations.contains(.init(interfaceOrientation: $0)) ? $0 : nil + } ?? previousInterfaceOrientation + Self.requestInterfaceOrientations( + .init(interfaceOrientation: targetOrientation), in: windowScene, updating: rootViewController + ) + } + + /// Asks the window scene to rotate its interface to one of the given orientations. + /// - Parameters: + /// - mask: The orientations to rotate to. + /// - windowScene: The window scene to rotate. + /// - viewController: The controller whose `supportedInterfaceOrientations` should be re-evaluated. + private static func requestInterfaceOrientations( + _ mask: UIInterfaceOrientationMask, in windowScene: UIWindowScene, updating viewController: UIViewController? + ) { + if #available(iOS 16.0, *) { + viewController?.setNeedsUpdateOfSupportedInterfaceOrientations() + windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) + } else { + let orientations: [UIInterfaceOrientation] = [ + .portrait, .landscapeRight, .landscapeLeft, .portraitUpsideDown + ] + guard let orientation = orientations.first(where: { mask.contains(.init(interfaceOrientation: $0)) }) + else { return } + UIDevice.current.setValue(orientation.rawValue, forKey: "orientation") + UIViewController.attemptRotationToDeviceOrientation() + } + } } diff --git a/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift b/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift index 2a21bfd..cf0d8c9 100644 --- a/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift +++ b/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift @@ -1,3 +1,4 @@ +import AVFoundation import XCTest @testable import OSBarcodeLib @@ -77,3 +78,28 @@ private extension OSBARCManagerTests { } } } + +final class OSBARCOrientationMappingTests: XCTestCase { + func testPreviewOrientationMapsToTheMatchingImageOrientation() { + XCTAssertEqual(CGImagePropertyOrientation(videoOrientation: .portrait), .rightMirrored) + XCTAssertEqual(CGImagePropertyOrientation(videoOrientation: .portraitUpsideDown), .leftMirrored) + XCTAssertEqual(CGImagePropertyOrientation(videoOrientation: .landscapeLeft), .upMirrored) + XCTAssertEqual(CGImagePropertyOrientation(videoOrientation: .landscapeRight), .downMirrored) + } + + func testDeviceOrientationMapsToTheMatchingInterfaceOrientation() { + XCTAssertEqual(UIInterfaceOrientation(deviceOrientation: .portrait), .portrait) + XCTAssertEqual(UIInterfaceOrientation(deviceOrientation: .portraitUpsideDown), .portraitUpsideDown) + XCTAssertEqual(UIInterfaceOrientation(deviceOrientation: .landscapeLeft), .landscapeRight) + XCTAssertEqual(UIInterfaceOrientation(deviceOrientation: .landscapeRight), .landscapeLeft) + XCTAssertNil(UIInterfaceOrientation(deviceOrientation: .faceUp)) + XCTAssertNil(UIInterfaceOrientation(deviceOrientation: .unknown)) + } + + func testInterfaceOrientationMapsToASingleOrientationMask() { + XCTAssertEqual(UIInterfaceOrientationMask(interfaceOrientation: .portrait), .portrait) + XCTAssertEqual(UIInterfaceOrientationMask(interfaceOrientation: .landscapeLeft), .landscapeLeft) + XCTAssertEqual(UIInterfaceOrientationMask(interfaceOrientation: .landscapeRight), .landscapeRight) + XCTAssertEqual(UIInterfaceOrientationMask(interfaceOrientation: .unknown), .all) + } +} From fe13fd7e03909caa61d94aacf4df25897245e6ba Mon Sep 17 00:00:00 2001 From: Andre Destro Date: Mon, 2 Nov 2026 11:05:32 +0000 Subject: [PATCH 2/2] fix: keep an allowed orientation in the pre-iOS 16 rotation fallback The fallback always picked the first orientation in the mask, so a forced landscape scanner opened with the interface in landscapeLeft was flipped 180 degrees to landscapeRight. Skip the rotation when the current orientation is already in the mask, and otherwise prefer how the device is held. --- ...eOrientationMask+OSBARCModelMappable.swift | 14 +++++++++++ .../OSBARCScannerViewHostingController.swift | 8 +++--- .../OSBARCManagerTests.swift | 25 +++++++++++++++++++ 3 files changed, 42 insertions(+), 5 deletions(-) diff --git a/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift b/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift index 161a405..77a9bf8 100644 --- a/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift +++ b/Sources/OSBarcodeLib/Scanner/Extensions/UIInterfaceOrientationMask+OSBARCModelMappable.swift @@ -22,6 +22,20 @@ extension UIInterfaceOrientationMask: OSBARCModelMappable { default: self = .all } } + + /// Orientation to rotate to so the interface fits this mask, or `nil` when the current one already does. + /// Prefers how the device is held, so a landscape mask doesn't flip an interface that's already landscape. + /// - Parameters: + /// - currentOrientation: The interface's current orientation. + /// - deviceOrientation: The device's physical orientation. + func interfaceOrientationToRequest( + from currentOrientation: UIInterfaceOrientation, deviceOrientation: UIDeviceOrientation + ) -> UIInterfaceOrientation? { + guard !self.contains(.init(interfaceOrientation: currentOrientation)) else { return nil } + let candidates = [UIInterfaceOrientation(deviceOrientation: deviceOrientation)].compactMap { $0 } + + [.portrait, .landscapeRight, .landscapeLeft, .portraitUpsideDown] + return candidates.first { self.contains(.init(interfaceOrientation: $0)) } + } } extension UIInterfaceOrientation { diff --git a/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift b/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift index 1b1fd52..5393d26 100644 --- a/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift +++ b/Sources/OSBarcodeLib/Scanner/OSBARCScannerViewHostingController.swift @@ -72,11 +72,9 @@ class OSBARCScannerViewHostingController: UIHostingController viewController?.setNeedsUpdateOfSupportedInterfaceOrientations() windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: mask)) } else { - let orientations: [UIInterfaceOrientation] = [ - .portrait, .landscapeRight, .landscapeLeft, .portraitUpsideDown - ] - guard let orientation = orientations.first(where: { mask.contains(.init(interfaceOrientation: $0)) }) - else { return } + guard let orientation = mask.interfaceOrientationToRequest( + from: windowScene.currentInterfaceOrientation, deviceOrientation: UIDevice.current.orientation + ) else { return } UIDevice.current.setValue(orientation.rawValue, forKey: "orientation") UIViewController.attemptRotationToDeviceOrientation() } diff --git a/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift b/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift index cf0d8c9..307dd95 100644 --- a/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift +++ b/Tests/OSBarcodeLibTests/OSBARCManagerTests.swift @@ -102,4 +102,29 @@ final class OSBARCOrientationMappingTests: XCTestCase { XCTAssertEqual(UIInterfaceOrientationMask(interfaceOrientation: .landscapeRight), .landscapeRight) XCTAssertEqual(UIInterfaceOrientationMask(interfaceOrientation: .unknown), .all) } + + func testOrientationToRequestKeepsAnOrientationAlreadyInTheMask() { + let mask = UIInterfaceOrientationMask.landscape + XCTAssertNil(mask.interfaceOrientationToRequest(from: .landscapeLeft, deviceOrientation: .landscapeRight)) + XCTAssertNil(mask.interfaceOrientationToRequest(from: .landscapeRight, deviceOrientation: .landscapeLeft)) + } + + func testOrientationToRequestPrefersHowTheDeviceIsHeld() { + let mask = UIInterfaceOrientationMask.landscape + let fromRight = mask.interfaceOrientationToRequest(from: .portrait, deviceOrientation: .landscapeRight) + let fromLeft = mask.interfaceOrientationToRequest(from: .portrait, deviceOrientation: .landscapeLeft) + XCTAssertEqual(fromRight, .landscapeLeft) + XCTAssertEqual(fromLeft, .landscapeRight) + } + + func testOrientationToRequestFallsBackWhenTheDeviceOrientationIsNotInTheMask() { + let flat = UIInterfaceOrientationMask.landscape.interfaceOrientationToRequest( + from: .portrait, deviceOrientation: .faceUp + ) + let landscape = UIInterfaceOrientationMask.portrait.interfaceOrientationToRequest( + from: .landscapeLeft, deviceOrientation: .landscapeLeft + ) + XCTAssertEqual(flat, .landscapeRight) + XCTAssertEqual(landscape, .portrait) + } }