SwiftUI Force Portrait On All Except One View
Asked Answered
V

6

27

I have a SwiftUI project. For all but one of the views, I want to allow portrait and only portrait mode. For only one view, I want to allow both portrait and landscape. There are some resources on Swift but I couldn't find any on SwiftUI.

Did anyone find a way to accomplish this?

Vagrom answered 7/1, 2020 at 17:36 Comment(1)
The current answers of this question is not working in IOS 16, and produces warning BUG IN CLIENT OF UIKIT: Setting UIDevice.orientation is not supported. Please use UIWindowScene.requestGeometryUpdate(_:) The solution can be find iOS 16 Scene orientation issueJube
A
34

I had to do something similar to this. Here's our approach.

We set the project orientation to only support Portrait mode.

Then in your AppDelegate add an instance variable for orientation and conform to the supportedInterfaceOrientationsFor delegate method.

static var orientationLock = UIInterfaceOrientationMask.portrait

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

Then when your about to present your landscape view perform the following actions:

AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()

And on dismissal,

AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()

Hope this helps!

Aflcio answered 7/1, 2020 at 18:27 Comment(9)
Thank you so much! I got an error when trying to do UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft, forKey: "orientation") but I didn't need that part really. The first line was all I needed.Vagrom
This is locking the device into landscape mode only, but OP requested allowing both portrait and landscape for the view. Is there a solution for allowing both portrait and landscape in this view?Puppetry
I'm getting an error: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__SwiftValue longLongValue]: unrecognized selector sent to instance 0x282de73c0' Any ideas?Jungian
In order to don't get the error, you have to put DispatchQueue.main.async in both the onAppear and onDisappear methods.Titfer
yep, it just works, thanks for this great solution. Bonus: it works on iOS 14, too. I know it should, but while I'm staring in disbelief at my broken app (that worked perfectly well under iOS 13) on Xcode 12 and iOS 14 many of my certainties have been shattered... so at least this works.Sexagenary
what i need to do if i want landscapeLeft & landscapeRight?Arteriosclerosis
Thanks for the great answer! Is there a way to not animate the rotation e.g. after the transition within a NavigationView, so that the destination View is already in the rotated state?Internationalism
@adelmachris, did you manage to find a solution? I'm having the exact problemTransalpine
Awesome, worked for me! However, I get a deprecated warning for UIViewController.attemptRotationToDeviceOrientation(). It tells me to use setNeedsUpdateOfSupportedInterfaceOrientations() on the view controller instance instead. But how do I get the instance of the view controller in a SwiftUI view, e.g. in .onAppear?Fraction
S
28

A couple of tweaks to the answer above:

In AppDelegate as Jonathan's answer above:

static var orientationLock = UIInterfaceOrientationMask.portrait

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

Then in the destinationView - the one that is landscape:

import SwiftUI

struct DestinationView: View {
    
    var body: some View {
        Group {
                
            Text("Hello")
                   
        }.onAppear {
            AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
            UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
            UINavigationController.attemptRotationToDeviceOrientation()
        }
        .onDisappear {
            DispatchQueue.main.async {
                AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
                UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
                UINavigationController.attemptRotationToDeviceOrientation()
            }
        }
    }
}

Note the .rawValue in the UIDevice line which gets rid of the LongValue error. Also, in the .onDisappear I had to use DispatchQueue.main.async in order to avoid an error when returning back to previous view which is in portrait.

Subserve answered 18/4, 2020 at 9:53 Comment(5)
Such a great answer Zenman! Simple, straightforward, and it "just works". Kudos for figuring this out. Shame on Apple for making this so difficult.Dingle
I used the same thing. Thanks a lot @Zenman C Minor change from what I faced. I had to wrap the onAppear part in DispatchQueue.main.async as well.Gaius
what i need to do if i want landscapeLeft & landscapeRight?Arteriosclerosis
For some reason this doesn't work the first few times I go into the view, after going in and out several times all of a sudden it starts to work, and then it keeps working. Very odd. I do add calls to main queue as well.Tedious
Turns out it works fine if I'm not running it from Xcode to device with debugger connected. In case someone else experience this issue. Try and disconnect phone and just run it on device.Tedious
A
9

If your app is using SwiftUI lifeCycle, the app is launched using a custom struct that conforms to the App protocol. Your app doesn't have AppDelegate which is needed to implement the above solutions.

To make your own AppDelegate try this code in your appnameApp.swift

@main
struct appnameApp: App {
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

class AppDelegate: NSObject, UIApplicationDelegate {
    
    static var orientationLock = UIInterfaceOrientationMask.portrait
    
    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil) -> Bool {
        // something to do
        return true
    }
    
    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window:UIWindow?) -> UIInterfaceOrientationMask {
        return AppDelegate.orientationLock
    }
}

Source: https://www.hackingwithswift.com/quick-start/swiftui/how-to-add-an-appdelegate-to-a-swiftui-app

Alabaster answered 23/6, 2021 at 15:42 Comment(0)
M
1

Plus, you can go further and use Jonathan's code to create an extension for the View class and simply call this extension on the classes you would like to rotate.

extension View {
    func unlockRotation() -> some View {
        onAppear {
            AppDelegate.orientationLock = UIInterfaceOrientationMask.allButUpsideDown
            UIViewController.attemptRotationToDeviceOrientation()
        }
        .onDisappear {
            AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
            UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
            UIViewController.attemptRotationToDeviceOrientation()
        }
    }
}
Manana answered 26/4, 2022 at 15:21 Comment(0)
D
0

use AppUtility from https://mcmap.net/q/158150/-how-to-lock-orientation-of-one-view-controller-to-portrait-mode-only

in SwiftUI

create custom UIHostingController

 class OrientationHostingController : UIHostingController<AnyView> {
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        AppUtility.lockOrientation(.portrait)
        
    }
 }




let rootView = OrientationHostingController(rootView: AnyView(SwiftUIView()))
Daubery answered 17/9, 2020 at 6:8 Comment(0)
K
0

To offer a slight change to Jonathan's great solution. This version answers folks above asking about how to use other orientations besides just "landscapeLeft".

First, Jonathan stated "We set the project orientation to only support Portrait mode." Don't do this if you want to allow other orientations. So for the project leave "Portrait", "Landscape Left" and "Landscape Right" checked along with "Upside Down" if you also wish this orientation.

Second, in the onAppear of the view you wish to ALLOW rotation, set the following:

AppDelegate.orientationLock = UIInterfaceOrientationMask.allButUpsideDown

The above replaces .landscapeLeft from the original example. Note, if you want to allow upside down, make it .all instead.

That's it. Everything else is as stated by Jonathan and others. The result is that the app will be portrait locked except for the views that you update with onAppear and the code above. For those the user will have access to all the orientations (or all but upside down).

Kook answered 11/1, 2023 at 1:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.