保持纵向视图但让 iOS 旋转 phone

Keep view portrait but let iOS Rotate with phone

我正在寻找一种无需旋转游戏视图即可旋转 iOS UI 的方法。

我正在开发的游戏支持多个方向,但游戏视图负责旋转以及与该方向变化相关的所有动画,并且应该始终呈现为纵向。

目前我将应用程序锁定为仅支持纵向,当 phone 旋转时,游戏也会旋转。但是主页栏和控制中心保持纵向,这还可以,但我希望该应用程序完全支持横向。

就像我现在一样,我听方向通知

func ListenToRotationUpdates() {
    UIDevice.current.beginGeneratingDeviceOrientationNotifications()
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(deviceOrientationDidChange),
        name: UIDevice.orientationDidChangeNotification,
        object: nil
    )
}

这将在游戏中设置方向

@objc func deviceOrientationDidChange() {
    let orientation = UIDevice.current.orientation

    if orientation == .unknown || orientation == .faceUp || orientation == .faceDown {
        return
    }

    self.gameScene.Set(orientation: orientation)
    setNeedsStatusBarAppearanceUpdate()
}

有没有办法在应用程序正常旋转 OS 时保持视图纵向?

这取决于如何旋转游戏视图。您是否使用一些自行处理的外部库?您是否重新计算视图框架和边界?所以你的想法的最终执行真的取决于这些因素。通常,在 UIViewController 中处理旋转的最常见方法是在 viewWillTransition 函数中,它处理所有对齐并同时发生的旋转动画。因此,如果您想以某种方式将视图重新计算回纵向尺寸,我建议使用此功能。这是一个例子:

    override func viewWillTransition(to size: CGSize,
                                 with coordinator: UIViewControllerTransitionCoordinator)
{
    coordinator.animate( alongsideTransition: { (UIViewControllerTransitionCoordinatorContext) -> Void in
        // calculate your view e.g. self.rotateToCurrentPosition()
        
    }, completion: { (UIViewControllerTransitionCoordinatorContext) -> Void in
        // e.g. self.view.layoutSubviews()
    })
    
    super.viewWillTransition(to: size, with: coordinator)
}

一种可能的解决方案是使用 windows.

将您的应用程序拆分为“旋转”部分和“非旋转”部分

这不是一个理想的选择,但最近我们获得的工具并没有给我们太多选择。使用此过程时您面临的问题是在呈现新视图控制器时可能会出现一些混乱。在你的情况下,这可能根本不是问题,但仍然......

简而言之,你所做的是:

  1. 保持主 window 不变,但让您的应用程序旋转到您需要的所有方向
  2. 创建一个只支持一个方向的视图控制器(无论你喜欢哪个),并在你的主要方向上但在状态栏下方显示一个新的window(默认行为)
  3. 创建一个具有透明背景的视图控制器,它可以将触摸事件传递到它下面的 window。还以新的 window 显示它,而不是前一个。此外,此 window 需要将触摸事件传递到底部 window。

您可以在代码中或使用故事板创建所有这些。但是有一些组件需要管理。这些都是我在验证这种方法时使用的:

class StandStillViewController: WindowViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
    }
    
    func setupManually() {
        self.view = DelegatedTouchEventsView()
        self.view.backgroundColor = UIColor.darkGray
        
        let label = UILabel(frame: .zero)
        label.text = "A part of this app that stands still"
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        view.addConstraint(.init(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
        view.addConstraint(.init(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 0.0))
        
        let button = UIButton(frame: .zero)
        button.setTitle("Test button", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(testButton), for: .touchUpInside)
        view.addSubview(button)
        view.addConstraint(.init(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
        view.addConstraint(.init(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: 50.0))
    }
    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
    override var shouldAutorotate: Bool { false }
    override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait }
    
    @IBAction private func testButton() {
        print("Button was pressed")
    }
    

}

这是底部的控制器。我希望这个会主持你的比赛。你需要保留

override var supportedInterfaceOrientations: UIInterfaceOrientationMask { return .portrait }
override var shouldAutorotate: Bool { false }
override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .portrait }

其余部分可能会更改、删除。


class RotatingViewController: WindowViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }
    
    func setupManually() {
        self.view = DelegatedTouchEventsView()
        self.view.backgroundColor = UIColor.clear // Want to see through it
        
        let label = UILabel(frame: .zero)
        label.text = "A Rotating part of this app"
        label.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(label)
        view.addConstraint(.init(item: label, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
        view.addConstraint(.init(item: label, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .top, multiplier: 1.0, constant: 60.0))
        
        let button = UIButton(frame: .zero)
        button.setTitle("Test rotating button", for: .normal)
        button.translatesAutoresizingMaskIntoConstraints = false
        button.addTarget(self, action: #selector(testButton), for: .touchUpInside)
        view.addSubview(button)
        view.addConstraint(.init(item: button, attribute: .centerX, relatedBy: .equal, toItem: view, attribute: .centerX, multiplier: 1.0, constant: 0.0))
        view.addConstraint(.init(item: button, attribute: .centerY, relatedBy: .equal, toItem: view, attribute: .centerY, multiplier: 1.0, constant: -50.0))
    }
    
    @IBAction private func testButton() {
        print("Rotating button was pressed")
    }

}

这个控制器将处理旋转的东西。你需要保留

self.view = DelegatedTouchEventsView()
self.view.backgroundColor = UIColor.clear // Want to see through it

也可以在情节提要中设置。你放在这个控制器上的任何东西都会随着你的设备旋转。例如,放置一些 GUI 东西的好地方。


class WindowViewController: UIViewController {
    
    private var window: UIWindow?
    
    func shownInNewWindow(delegatesTouchEvents: Bool, baseWindow: UIWindow? = nil) {
        let scene = baseWindow?.windowScene ?? UIApplication.shared.windows.first!.windowScene!
        let newWindow: UIWindow
        if delegatesTouchEvents {
            newWindow = DelegatedTouchEventsWindow(windowScene: scene)
        } else {
            newWindow = UIWindow(windowScene: scene)
        }
        
        
        newWindow.rootViewController = self
        newWindow.windowLevel = .normal
        newWindow.makeKeyAndVisible()
        
        self.window = newWindow
    }
    
    func dismissFromWindow(completion: (() -> Void)? = nil) {
        removeFromWindow()
        completion?()
    }
    
    func removeFromWindow() {
        self.window?.isHidden = true
        self.window = nil
    }
    
}

这是我用作上述两个视图控制器的基础 class。它并不多,但它允许视图控制器显示在新的 window 中。这段代码是从我的一个旧项目中粘贴的,可以进行一些小的改进。但它确实有效...


class DelegatedTouchEventsView: UIView {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        return view == self ? nil : view
    }
    
}

class DelegatedTouchEventsWindow: UIWindow {

    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let view = super.hitTest(point, with: event)
        return view == self ? nil : view
    }
    
}

这是让触摸事件通过的两个子class。快速解释这是如何工作的:当收到事件时,您的系统将首先通过您的视图层次结构发送此事件,询问“谁将处理此事件?”。返回 nil 表示“不是我”,UIView 的默认值为 self。所以在这段代码中我们说:“如果我的任何子视图想要处理这个事件(比如按钮)那么它可能会处理这个事件。但是如果它们中的 none 想要处理它们那么我也不会。” UIWindowUIView 的子class,所以我们需要同时处理它们。


class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
//        let standingStillController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "StandStillViewController") as! StandStillViewController
//        standingStillController.shownInNewWindow(delegatesTouchEvents: false, baseWindow: self.view.window)
//
//        let rotatingViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "RotatingViewController") as! RotatingViewController
//        rotatingViewController.shownInNewWindow(delegatesTouchEvents: true, baseWindow: self.view.window)
        
        let standingStillController = StandStillViewController()
        standingStillController.setupManually()
        standingStillController.shownInNewWindow(delegatesTouchEvents: false, baseWindow: self.view.window)
        
        let rotatingViewController = RotatingViewController()
        rotatingViewController.setupManually()
        rotatingViewController.shownInNewWindow(delegatesTouchEvents: true, baseWindow: self.view.window)
    }
    
    
}

这是一个关于如何一起使用它们的示例。正如承诺的那样,无论是使用 Storyboards 还是手动,两者都应该有效。

看起来工作量很大,但您需要设置一次,再也不会看它了。