I am working on an app which uses viewcontrollers and I am slowly migrating to use views built using SwiftUI.
I am working on new swiftUI views, so i am setting UIHostingController as the rootViewController in the SceneDelegate
Overall my whole app is restricted to portrait only here
There are some individual views that i want to allow to be shown in portrait and landscape left
This is what i have used so far for swiftUI, that works perfectly in iOS 15
in AppDelegate i have
static var orientationLock = UIInterfaceOrientationMask.portrait
then in the method
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask
this is set to return
return AppDelegate.orientationLock
Then in the particular swiftUI file i want to allow rotation, i simply have to do this to handle screen rotations by adding this modifier to the main view
.onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
if orientation.isLandscape {
AppDelegate.orientationLock = UIInterfaceOrientationMask.landscapeLeft
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
} else {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
.onDisappear {
DispatchQueue.main.async {
AppDelegate.orientationLock = UIInterfaceOrientationMask.portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
UINavigationController.attemptRotationToDeviceOrientation()
}
}
However iOS 16 has depreciated the code:
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
we get an error message, BUG IN CLIENT OF UIKIT: Setting UIDevice.orientation is not supported. Please use UIWindowScene.requestGeometryUpdate(_:)
we are now told in Apples documentation to simply call the code below from a UIViewcontroller:
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeLeft))
however this code simply does not work for me, i want to be able to call the code from the swiftUI file, and when i try calling the code from the swiftUI file, nothing happens. How can i make this work? To be clear, i need to be able to call a method/modifier from within the swiftUI file, that will allow the screen to rotate just for that screen (i.e. when I navigate to another screen, it will be back to portrait)
I think i have to try to access UIHostingController somehow from the swiftUI file and call requestGeometryUpdate via that, I have tried different ways of doing this, but clearly what i've tried so far does not work!
Related
In iOS swift 5 project I need to support multiple device orientations in only one scene.
I have the following requirements:
If device is rotated, device orientation should not be changed.
If button is tapped, device orientation should be changed (left
rotation)
This means that in only one UIViewController user should be able to change device orientation manually by tapping on a button, but rotating the device should not do anything.
One simple way to accomplish this is by setting your supported orientation on the AppDelegate (_:supportedInterfaceOrientationsFor:)
Create a local variable there
var orientation: UIInterfaceOrientationMask = .portrait
and then return that variable as supported orientation
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return orientation
}
on the ViewController that you want to rotate with a touch of a button, you can change the supported orientation on the app delegate and than force the device orientation to change so that the view is rotated
private func changeSupportedOrientation() {
let delegate = UIApplication.shared.delegate as! AppDelegate
switch delegate.orientation {
case .portrait:
delegate.orientation = .landscapeLeft
default:
delegate.orientation = .portrait
}
}
#IBAction func rotateButtonTapped(_ sender: UIButton) {
changeSupportedOrientation()
switch UIDevice.current.orientation {
case .landscapeLeft:
UIDevice.current.setValue(UIDeviceOrientation.portrait.rawValue, forKey: "orientation")
default:
UIDevice.current.setValue(UIDeviceOrientation.landscapeLeft.rawValue, forKey: "orientation")
}
}
this will force the orientation of the device to change and then rotate your view. If you tap on the button again the orientation will be back to .portrait
Please be careful of using this as you need to really consider your navigation stack to make sure that only the top of the navigation stack support rotation and can only be pop from the navigation stack after the orientation is set back to the original which is .portrait only.
The problem I am having is a bit strange and I can't seem to find any good solutions out there. So I have two UIViewControllers, one is allowed to take all orientations, another can only be viewed in landscape
internal override func shouldAutorotate() -> Bool {
return true
}
internal override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
return UIInterfaceOrientationMask.Lanscape
}
If I am in portrait mode on ViewController1, and I push VC2, it rotates just fine, but when I leave VC2, VC1 is stuck in landscape even though device itself is in portrait mode. How can I fix it? I tried calling AttemptToRotateToDeviceOrientation method in ViewDidAppear method of VC1, but it doesn't do anything
You just need to create app delegate instance in your view controller like:
let appDel = UIApplication.shared.delegate as! AppDelegate
On Viewwillappear()of view controller just add this code for landscape
appDel.myOrientation = .landscape
UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
And for portrait add this code
appDel.myOrientation = .portrait
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
I am trying to prevent rotation (lock it to say, portrait) in a specific VC that is
embedded in a navigation controller.
I am currently doing this:
To UINavigationController
extension UINavigationController {
public override func supportedInterfaceOrientations() -> Int {
return visibleViewController.supportedInterfaceOrientations()
}
}
In my VC:
class ViewController: UIViewController {
override func supportedInterfaceOrientations() -> Int {
return Int(UIInterfaceOrientationMask.Landscape.rawValue)
}
}
However, I have issues when we go to another VC (embedded in another nav controller) is presented which supports both landscape and portrait. Suppose, the user rotates in the new screen to landscape. And clicks back to go to original screen. The app is now presented in landscape as opposed to portrait defined in its supportedInterfaceOrientations override. How do I prevent this erroneous behaviour?
I read in iOS 11, we should use viewWillTransition(to:with:) to handle rotation (and locking as well). In UIViewController documentation
“As of iOS 8, all rotation-related methods are deprecated. Instead,
rotations are treated as a change in the size of the view controller’s
view and are therefore reported using the viewWillTransition(to:with:)
method. When the interface orientation changes, UIKit calls this
method on the window’s root view controller. That view controller then
notifies its child view controllers, propagating the message
throughout the view controller hierarchy.”
Can you give directions on how to achieve it?
You can use this cool utility that I've been using.
struct AppUtility {
static func lockOrientation(_ orientation: UIInterfaceOrientationMask) {
if let delegate = UIApplication.shared.delegate as? AppDelegate {
delegate.orientationLock = orientation
}
}
/// OPTIONAL Added method to adjust lock and rotate to the desired orientation
static func lockOrientation(_ orientation: UIInterfaceOrientationMask, andRotateTo rotateOrientation:UIInterfaceOrientation) {
self.lockOrientation(orientation)
UIDevice.current.setValue(rotateOrientation.rawValue, forKey: "orientation")
}
}
You can also rotate your screen and at the same time, lock it to that orientation. I hope this helps!
You can use supportedInterfaceOrientationsFor
Define this in your AppDelegate
var restrictRotation = Bool()
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if restrictRotation {
return .portrait
}
else {
return .all
}
}
Put below code in your ViewController
func restrictRotation(_ restriction: Bool) {
let appDelegate = UIApplication.shared.delegate as? AppDelegate
appDelegate?.restrictRotation = restriction
}
call above function in your ViewController ViewwillAppear like this.
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
self.restrictRotation(true) // TRUE MEANS ONLY PORTRAIT MODE
//OR
self.restrictRotation(false) // FALSE MEANS ROTATE IN ALL DIRECTIONS
}
I have a simple camera app.
The Camera Preview screen is a viewcontroller (name ViewController) which I locked the orientation via
this code (can't find the StackOverflow link to this code), The viewcontroller is embedded in a navigation controller:
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if let navigationController = self.window?.rootViewController as? UINavigationController {
if navigationController.visibleViewController is ViewController {
return UIInterfaceOrientationMask.portrait
} else {
return UIInterfaceOrientationMask.all
}
}
return UIInterfaceOrientationMask.portrait
}
When the camera starts up, the Camera Preview is locked in .portrait orientation, which is what I desire.
However, if I hit the library button, which segues me to another screen,
and change the orientation of the library screen by rotating my physical device, and THEN return back to my Camera Preview screen via the "back" button, the orientation is broken. It's in landscape mode, instead of portrait mode.
How do I get a hard lock?
Just incase, I made a dummy model of my code here (too long to post here), where you guys can test it out yourself
https://github.com/bigmit2011/Testing-Camera
EDIT:
If the app crashes just try running it one more time.
For some reason the first time the app is ran it crashes. Thank you.
EDIT2:
Attempting to set embedded ViewController as a delegate of UINavigationController.
However, I don't quite understand if I'm doing it correctly:
class ViewController: UIViewController, UINavigationControllerDelegate {
var navigation: UINavigationController?
self.navigation.delegate = self // doesn't seem to work
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return .portrait
}
Do I need to create a separate swift file for the UiNavigationController?
EDIT3:
Following Matt's answer code is as follows:
class CameraViewController :UIViewController, UINavigationControllerDelegate {
override func viewDidLoad() {
self.navigationController?.delegate = self
super.viewDidLoad()
func navigationControllerSupportedInterfaceOrientations(_ navigationController: UINavigationController) -> UIInterfaceOrientationMask {
return .portrait
}
However, this seems to lock everything in portrait mode not just that single viewController embedded within Navigation Controller.
You are making two mistakes.
First, you are trying to govern the orientation of individual view controllers from the app delegate. That's wrong. The app delegate governs the totality of possible orientations for the whole app. But as for view controllers, each individual view controller governs its own orientation. Just implement supportedInterfaceOrientations in each of your two view controllers (not in the app delegate).
Second, your first view controller is in a navigation interface. Therefore it is not the top-level view controller and cannot govern orientation directly (with supportedInterfaceOrientations). The correct way to govern the orientation of a navigation interface is to set yourself as the navigation controller's delegate and implement navigationControllerSupportedInterfaceOrientations(_:).
I also couldn't run your app. But I tested this in Xcode with a main VC, a 2nd VC and embedded the main VC in a navigation controller. I then followed the advice in this article Selective rotation lock in iOS
You add the following to AppDelegate:
var enableAllOrientation = false
func application(_ application: UIApplication,supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
if (enableAllOrientation == true){
return UIInterfaceOrientationMask.allButUpsideDown
}
return UIInterfaceOrientationMask.portrait
}
Then in the VC that allows allButUpsideDown (or change this to what you want), you add the following code:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.enableAllOrientation = true
}
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
let appDelegate = UIApplication.shared.delegate as! AppDelegate
appDelegate.enableAllOrientation = false
let value = UIInterfaceOrientation.portrait.rawValue
UIDevice.current.setValue(value, forKey: "orientation")
}
I put this in the 2nd VC. Ran the app in the simulator, segued to the 2nd VC, changed the orientation to landscape then hit back and the main VC was locked in portrait.
First of all, my project hierarchy:
tab bar controller -> navigation master -> view controllers
I have one page I want to rotate in landscape mode.
My project is compatible with all the device orientation modes.
I've manually frozen all the view controllers with autorotation returning false:
override func shouldAutorotate() -> Bool {
return false
}
except the one I want to rotate. S
I'm trying this in the view controller I want to be displayed in landscape:
let value = UIInterfaceOrientation.LandscapeLeft.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
But does nothing.
Also, I experimented an issue with this method. When the app was starting in landscape, the view was displaying in landscape and was unable to rotate. I manage to fix it with this method In my app delegate:
func application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) -> UIInterfaceOrientationMask {
if(self.restrictRotation == false){
return UIInterfaceOrientationMask.Portrait//UIInterfaceOrientationMaskPortrait
}
else{
return UIInterfaceOrientationMask.All//UIInterfaceOrientationMaskAll
}
}
And in viewDidLoad setting the var to true where I want to allow rotations:
let appDelegate = UIApplication.sharedApplication().delegate as! AppDelegate
appDelegate.restrictRotation = true
let value = UIInterfaceOrientation.LandscapeLeft.rawValue
UIDevice.currentDevice().setValue(value, forKey: "orientation")
I've tested with the debugger and the method application(application: UIApplication, supportedInterfaceOrientationsForWindow window: UIWindow?) is called, but the app don't rotate. I guess is because with a tab bar and a navigation controller, things has to be done differently, but don't know how exactly. Can anyone help with this?