iOS 16+ 中以横向模式仅在单个屏幕上显示 UIViewController 不起作用 [Swift 5.7]

8

iOS 16之前,以横屏模式呈现单个屏幕对于纵向应用程序来说是可以的。下面是工作代码。

备注整个应用程序仅限于纵向模式。

override public var shouldAutorotate: Bool {
    return false
}

override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
    return .landscapeLeft
}

override public var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation {
    return .landscapeLeft
}

我找到了解决方案,但它是针对UIWindowScene的,而我需要在UIWindow中找到解决方案。我需要帮助在iOS 16中修复它。
Xcode - 14.0, iOS - 16.0, 模拟器 - 14 Pro
如果有人需要,我可以准备演示文稿。

你是否成功让它在UIWindowScene中工作?我收到了一个错误,指出所请求的方向不受支持。 - Gutty1
@Gutty1 我只在使用UIWindow。很抱歉我从未尝试过UIScene。 - Chetan Prajapati
这非常棘手,没有简单的解决方案。 - Fattie
@Fattie 你可以参考我的答案。(如果你有更多的想法,请提出建议) - Chetan Prajapati
这个回答可能对你有帮助,请参考 https://dev59.com/XlEG5IYBdhLWcg3wH0fT#76329324 - Amrit Tiwari
5个回答

5

这可以用两种方法完成。

1. 我个人更喜欢这种方式

1.1 将此功能保留在AppDelegate中以处理方向(这是必须的)

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

1.2 在你希望强制屏幕方向的 ViewController 中,前往该视图控制器并在变量声明部分添加以下代码:

var forceLandscape: Bool = false

override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
           forceLandscape ? .landscape : .portrait
}

我们将更新forceLandscape,以便其得到更新,然后也会更新supportedInterfaceOrientations
在此,我们正在设置触发器以更新forceLandscape(我们可以将这些代码行添加到按钮操作中以处理iOS 16强制旋转)。
if #available(iOS 16.0, *) {
            self.forceLandscape = true
            guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { return }
            self.setNeedsUpdateOfSupportedInterfaceOrientations()
            DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: {
                windowScene.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight)){
                        error in
                        print(error)
                        print(windowScene.effectiveGeometry)
                }
            })

这将更新forceLandscape,因此它将检查方向并根据其更新

上述代码行是其中一种方法,另一种方法如下:

2. 另一种方法是在AppDelegate类中更新方向:

2.1 在AppDelegate中保留此函数和属性以处理方向(这是必须的)

var orientation : UIInterfaceOrientationMask = .portrait
        func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
            return orientation
        }

2.2 在视图控制器按钮操作中,我们可以更新属性。

@IBAction func buttonAction(_ sender: Any) {
        let appDel = UIApplication.shared.delegate as! AppDelegate
        appDel.orientation = .landscape

        if #available(iOS 16.0, *) {
            DispatchQueue.main.async {
                let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
                self.setNeedsUpdateOfSupportedInterfaceOrientations()
                self.navigationController?.setNeedsUpdateOfSupportedInterfaceOrientations()
                windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) { error in
                    print(error)
                    print(windowScene?.effectiveGeometry ?? "")
                }
            }
        }else{
            UIDevice.current.setValue(UIInterfaceOrientation.landscapeLeft.rawValue, forKey: "orientation")
        }

5
如何让此功能在使用SwiftUI制作的特定屏幕上运行? - NNN

1

经过多次尝试,我找到了简单的解决方案。正如我在问题中提到的,我的整个应用程序只能以纵向模式显示,并且只有一个屏幕需要以横向模式呈现。

这段代码不需要任何外部窗口来进行makeKeyAndVisible操作。 如果你使用额外的窗口来呈现,则需要为iOS 16单独编写dismiss代码。

旧代码在之前的iOS 16版本中可以继续使用,没有任何改变。

以下是神奇的代码行。

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    if let _ = window?.rootViewController?.presentedViewController as? LandscapeChartVC {
        if #available(iOS 16.0, *) {
            return .landscapeLeft
        } else {
            return .portrait
        }
    } else {
        return .portrait
    }
}

我已经在appDelegate的supportedInterfaceOrientationsFor中确定了我的横向视图控制器。

你可以更改单词presentedViewController来获取你的控制器。就是这样。

使用以下代码为iPad添加所有3或4个方向的支持:

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    if UIDevice.IS_IPAD {
        return .allButUpsideDown
    } else {
        if let _ = window?.rootViewController?.presentedViewController as? LandscapeChartVC {
            if #available(iOS 16.0, *) {
                return .landscapeLeft
            } else {
                return .portrait
            }
        } else {
            return .portrait
        }
    }

如果需要iPad应用程序锁定方向,可以使用iPhone /以上代码。

这个想法得出了答案,感谢所有分享兴趣的人。如果有人仍然有更好的解决方案,我很乐意更新。


太棒了,这对我有用。我也在尝试同样的事情,但是针对全屏视频播放器。在我的情况下,我需要在AppDelegate中的函数中添加以下内容:if ((window?.rootViewController?.presentedViewController?.isKind(of: NSClassFromString("AVFullScreenViewController")!)) != nil) - Tomas
最终对我来说不起作用,因为"AVFullScreenViewController"似乎也被Google广告和Google登录使用,所以如果我使用这段代码,它将会旋转这两个视图。 - Tomas

1

我想到的从纵向呈现模态视图控制器的最佳解决方案是将setNeedsUpdateOfSupportedInterfaceOrientations(), requestGeometryUpdate(.iOS(interfaceOrientations: .landscape)) 的组合应用于新窗口下的 AppDelegate,并允许接口方向。

AppDelegate:

var allowedOrientation: UIInterfaceOrientationMask = .allButUpsideDown

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

在横屏模式下呈现视图控制器


var overlayWindow: UIWindow? // New window to present the controller in


func presentModalInLandscape(vc: ViewController) {
    if #available(iOS 16.0, *) {
        let appdelegate = UIApplication.shared.delegate as! AppDelegate
        appDelegate.allowedOrientation = .landscapeRight

        if let currentWindowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene {
            overlayWindow = UIWindow(windowScene: currentWindowScene)
        }

        overlayWindow?.windowLevel = UIWindow.Level.alert
        overlayWindow?.rootViewController = livevc

        overlayWindow?.makeKeyAndVisible()
        // It's important to do it after the interface has enough time to rotate
        DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
           self.overlayWindow?.windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .landscapeRight))
           vc.setNeedsUpdateOfSupportedInterfaceOrientations()
        }

    } else {
        // For iOS 15 it's enough to present it modally
        self.present(vc, animated: true, completion: nil)
    }
}


当您想要关闭它时,您需要执行以下操作。

if #available(iOS 16.0, *) {
    self.overlayWindow?.isHidden = true // Destroy the window
    self.overlayWindow?.windowScene = nil
    self.overlayWindow = nil 
    appDelegate().allowedOrientation = .allButUpsideDown // Reset allowed orientation
    self.setNeedsUpdateOfSupportedInterfaceOrientations() // Set the controller back
} else {
    self.presentedViewController?.dismiss(animated: true)
}

仍然不是完全的百分之百,因为视图控制器先以横向方式呈现,然后闪回到纵向方式,一秒钟后再次旋转至横向方式。但是,没有UIWindow时,有时会在锁定横向模式之前进行两次此操作。


1
我在 iOS 16 发布说明中找到了一些相关的内容。
https://developer.apple.com/documentation/ios-ipados-release-notes/ios-16-release-notes?changes=lat__8_1
UIKit 中有一些已弃用的内容:

已弃用内容
[UIViewController shouldAutorotate] 已被弃用,不再受支持。[UIViewController attemptRotationToDeviceOrientation] 已被弃用,并替换为 [UIViewController setNeedsUpdateOfSupportedInterfaceOrientations]。
解决方法:依赖 shouldAutorotate 的应用程序应使用视图控制器 supportedInterfaceOrientations 反映其首选项。如果支持的方向发生变化,请使用 `-[UIViewController setNeedsUpdateOfSupportedInterface

我认为你可能需要使用 setNeedsUpdateOfSupportedInterface

1
我已经尝试过这个,但是不起作用... - Gutty1
不,这真的不起作用。 - Chetan Prajapati

0

我想要锁定的ViewController位于一个UINaviationController中。对于这种情况,这是我的解决方案:

我有一个struct,其中包含lockunlock方法。


struct AppOrientation {
    
    // statusBarOrientation
    // 0 - unknown
    // 1 - portrait
    // 2 - portraitUpsideDown
    // 3 - landscapeLeft
    // 4 - landscapeRight
    
    
    static let ORIENTATION_KEY: String = "orientation"
    
    private static func lockInterfaceOrientation(_ orientation: UIInterfaceOrientationMask) {
        
        if let delegate = UIApplication.shared.delegate as? AppDelegate {
            delegate.orientationLock = orientation
        }
    }
        
//    Important
//    Notice that UIDeviceOrientation.landscapeRight is assigned to UIInterfaceOrientation.landscapeLeft
//    and UIDeviceOrientation.landscapeLeft is assigned to UIInterfaceOrientation.landscapeRight.
//    The reason for this is that rotating the device requires rotating the content in the opposite direction.
    
    // Note that UIInterfaceOrientationLandscapeLeft is equal to UIDeviceOrientationLandscapeRight (and vice versa).
    // This is because rotating the device to the left requires rotating the content to the right.
    
    // DevieOrientation
    // 0 - unknown
    // 1 - portrait
    // 2 - portraitUpsideDown
    // 3 - landscapeLeft
    // 4 - landscapeRight
    // 5 - faceUp
    // 6 - faceDown
    
    // UIInterfaceOrientation
    // - landscapeLeft:
    // -- Home button on the left
    // - landscapeRight:
    // -- Home button on the right
    
    // UIDevice orientation
    // - landscapeLeft:
    // -- home button on the right
    // - landscapeRight:
    // -- home button on the left
    
    static func lockDeviceToLandscapeLeft() {
        
        if #available(iOS 16.0, *) {
            let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            
            if let controller = windowScene?.keyWindow?.rootViewController as? RMNavigationController {
                controller.lockLandscape = true
            }
        } else {
            UIDevice.current.setValue(UIDeviceOrientation.landscapeLeft.rawValue, forKey: ORIENTATION_KEY)
            lockInterfaceOrientation(.landscapeRight)
        }
        

        
        // the docs say you should call 'attemptRorationToDeviceOrientation()
        // lots of StackOverflow answers don't use it,
        // but a couple say you _must_ call it.
        // for me, not calling it seems to work...
//        UINavigationController.attemptRotationToDeviceOrientation()
    }
    
    static func unlockOrientation() {
        if #available(iOS 16.0, *) {
            let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
            
            if let controller = windowScene?.keyWindow?.rootViewController as? RMNavigationController {
                controller.lockLandscape = false
            }
        } else {
            lockInterfaceOrientation(.all)
        }
    }
}

iOS 16之前,您只需要调用这两个方法。

从iOS 16开始,您现在还需要调用setNeedsUpdateOfSupportedInterfaceOrientations()


override func viewWillAppear(_ animated: Bool) {
  super.viewWillAppear(animated)
  AppOrientation.lockDeviceToLandscapeLeft()
        
  if #available(iOS 16.0, *) {
    self.setNeedsUpdateOfSupportedInterfaceOrientations()
  } else {
    // Fallback on earlier versions
  }
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    AppOrientation.unlockOrientation()
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接