Swift - 如何检测屏幕方向变化

141

我想将两个图像添加到单个图像视图中(即横向一个图像,纵向另一个图像),但我不知道如何使用Swift语言检测方向更改。

我尝试了这个答案,但它只需要一个图像。

override func viewWillTransitionToSize(size: CGSize, withTransitionCoordinator coordinator: UIViewControllerTransitionCoordinator) {
    if UIDevice.currentDevice().orientation.isLandscape.boolValue {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

我是iOS开发新手,非常感谢任何建议!


1
请问您能否编辑您的问题并格式化您的代码吗?当您选择代码时,您可以使用cmd+k进行格式化。 - jbehrens94
2
请查看以下内容: https://dev59.com/Pl8e5IYBdhLWcg3whqo4 - DSAjith
它能正确打印横向/纵向吗? - Daniel
是的,它正确地打印了 @simpleBob - Raghuram
你应该使用界面方向而不是设备方向 => https://dev59.com/2VkT5IYBdhLWcg3wPdCU#60577486 - Wilfried Josset
如果您想在应用程序启动时立即检测更改,请检查此选项。 https://dev59.com/clsW5IYBdhLWcg3w7KtL#49058588 - eharo2
15个回答

166
let const = "Background" //image name
let const2 = "GreyBackground" // image name
    @IBOutlet weak var imageView: UIImageView!
    override func viewDidLoad() {
        super.viewDidLoad()

        imageView.image = UIImage(named: const)
        // Do any additional setup after loading the view.
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        if UIDevice.current.orientation.isLandscape {
            print("Landscape")
            imageView.image = UIImage(named: const2)
        } else {
            print("Portrait")
            imageView.image = UIImage(named: const)
        }
    }

谢谢,它对imageView有效,如果我想要为viewController添加背景图片怎么办? - Raghuram
1
将一个imageView放在背景上。为imageView设置约束,使其顶部、底部、前导和尾随到ViewController的主视图,以覆盖整个背景。 - Rutvik Kanbargi
4
请在您的重写方法中调用super.viewWillTransitionToSize,不要忘记。 - Brian Sachetta
5
还有另一种方向类型:.isFlat。如果您只想捕获“纵向”方向,建议您将else子句更改为else if UIDevice.current.orientation.isPortrait。注意:.isFlat可能在纵向和横向模式下都发生,并且仅使用 else {} 将始终默认到那里(即使是平面横向)。 - PMT
为了处理所有可能的情况,您应该使用界面方向而不是其他方法。=> https://dev59.com/2VkT5IYBdhLWcg3wPdCU#60577486 - Wilfried Josset
显示剩余3条评论

106

使用 NotificationCenterUIDevicebeginGeneratingDeviceOrientationNotifications

Swift 4.2+

override func viewDidLoad() {
    super.viewDidLoad()        

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: UIDevice.orientationDidChangeNotification, object: nil)
}

deinit {
   NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)         
}

func rotated() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

Swift 3

override func viewDidLoad() {
    super.viewDidLoad()        

    NotificationCenter.default.addObserver(self, selector: #selector(ViewController.rotated), name: NSNotification.Name.UIDeviceOrientationDidChange, object: nil)
}

deinit {
     NotificationCenter.default.removeObserver(self)
}

func rotated() {
    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

3
因为 viewWillTransition: 预计屏幕方向会变化,而 UIDeviceOrientationDidChange 则在屏幕方向变化完成后才被调用。 - Lyndsey Scott
5
这个答案中有趣的地方是,即使视图控制器的 shouldAutorotate 被设置为 false,它仍然可以提供当前的屏幕方向。这对于像相机应用程序的主屏幕这样的屏幕非常方便--屏幕方向被锁定,但按钮可以旋转。 - yoninja
3
自iOS 9起,您甚至不需要显式调用deinit。 您可以在此处阅读更多信息:https://useyourloaf.com/blog/unregistering-nsnotificationcenter-observers-in-ios-9/ - James Jordan Taylor
1
在iPad上不会调用viewWillTransition(to)函数 - 因为它只有在尺寸类别更改时才会被调用 - 如果您需要检测iPad的方向,这是正确的答案。否则,您需要为相关视图返回自定义尺寸类别。 - LightningStryk
1
自从Swift 4.2以来,通知名称已从NSNotification.Name.UIDeviceOrientationDidChange更新为UIDevice.orientationDidChangeNotification - dchakarov
显示剩余8条评论

97

⚠️设备方向 != 界面方向⚠️

Swift 5.* iOS16 及以下版本

你应该明确区分:

  • 设备方向 => 表示物理设备的方向
  • 界面方向 => 表示屏幕上显示的界面方向

有许多情况下这两个值不匹配,比如:

  • 当你锁定屏幕方向时
  • 当你将设备放平时

在大多数情况下,你会想要使用界面方向,你可以通过 window 获取它:

private var windowInterfaceOrientation: UIInterfaceOrientation? {
    return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
}

如果您也想支持 < iOS 13(例如iOS 12),则应执行以下操作:

private var windowInterfaceOrientation: UIInterfaceOrientation? {
    if #available(iOS 13.0, *) {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
    } else {
        return UIApplication.shared.statusBarOrientation
    }
}

现在你需要定义在哪里对窗口界面方向的更改做出反应。有多种方法可以实现,但最佳解决方案是在willTransition(to newCollection: UITraitCollection中执行。

这个继承的UIViewController方法可以被覆盖,每次接口方向更改时都会触发它。因此,您可以在后者中进行所有修改。

以下是一个解决方案示例:

class ViewController: UIViewController {
    override func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator) {
        super.willTransition(to: newCollection, with: coordinator)
        
        coordinator.animate(alongsideTransition: { (context) in
            guard let windowInterfaceOrientation = self.windowInterfaceOrientation else { return }
            
            if windowInterfaceOrientation.isLandscape {
                // activate landscape changes
            } else {
                // activate portrait changes
            }
        })
    }
    
    private var windowInterfaceOrientation: UIInterfaceOrientation? {
        return UIApplication.shared.windows.first?.windowScene?.interfaceOrientation
    }
}

通过实现这种方法,您将能够对接口的任何方向变化做出反应。但请记住,在打开应用程序时不会触发它,因此您还需要在 viewWillAppear() 中手动更新您的界面。

我创建了一个示例项目,突显设备方向和界面方向之间的区别。此外,它将帮助您了解根据您决定何时更新UI的生命周期步骤而产生的不同行为。

随意克隆并运行以下存储库: https://github.com/wjosset/ReactToOrientation


3
我认为这个答案是最好和最有信息量的。但是在iPadOS13.5上测试,使用建议的func willTransition(to newCollection: UITraitCollection, with coordinator: UIViewControllerTransitionCoordinator)并没有起作用。我已经使用func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator)使其工作。 - Andrej
你好@WilfriedJosset,我尝试了这个解决方案,但当手机方向锁定时它不起作用。你能帮我吗? - Khadija Daruwala
@PersianBlue 嘿!如果用户锁定了手机方向,那么它不应该旋转/动画/触发任何东西,这对我来说听起来很合乎逻辑,但我可能没有完全理解你的问题范围。你所说的“不起作用”是什么意思? - Wilfried Josset
@PersianBlue 针对这种特定的用例,您可能想尝试观察设备方向(而不是界面)=> https://dev59.com/2VkT5IYBdhLWcg3wPdCU#40263064 - Wilfried Josset
1
@PersianBlue 嗯,你可以尝试使用CoreMotion来进行黑客攻击 => https://medium.com/@maximbilan/how-to-use-core-motion-in-ios-using-swift-1287f7422473 但是可能会被苹果拒绝。 - Wilfried Josset
显示剩余6条评论

80

Swift 3 以上代码已更新:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    if UIDevice.current.orientation.isLandscape {
        print("Landscape")
    } else {
        print("Portrait")
    }
}

1
你知道为什么我在子类化UIView时无法覆盖viewWillTransition方法吗? - Xcoder
@JamesJordanTaylor,你能解释一下为什么它会崩溃吗? - orium
我评论时已经是6个月前的事了,但我认为当我尝试使用Xcode时,它给出的原因是空指针异常,因为视图本身尚未被实例化,只有viewController。不过我可能记错了。 - James Jordan Taylor
@Xcoder,这是UIViewController上的一个函数,而不是UIView - 这就是为什么你无法覆盖它的原因。 - LightningStryk
1
请注意,界面方向和设备方向是有区别的。 - Pierre
显示剩余2条评论

15

Swift 4+:我在软键盘设计中使用了这个方法,但是不知什么原因,UIDevice.current.orientation.isLandscape 方法一直返回 Portrait,所以我用了下面的方法代替:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)

    if(size.width > self.view.frame.size.width){
        //Landscape
    }
    else{
        //Portrait
    }
}

我在我的iMessage应用程序中也有类似的问题。这不是很奇怪吗?而且,你的解决方案甚至起到了相反的作用!¯_(ツ)_/¯ - Shyam
不要使用UIScreen.main.bounds,因为它可能会给出转换之前的屏幕边界(注意方法中的“Will”),特别是在多次快速旋转时。你应该使用“size”参数... - Dan Bodnar
@dan-bodnar 你是指nativeBounds参数吗? - asetniop
3
@asetniop,不。您上面编写的viewWillTransitionToSize:withCoordinator:方法中注入的“size”参数将反映出过渡后您的视图确切的大小。 - Dan Bodnar
1
如果您正在使用子视图控制器,这并不是一个好的解决方案,容器的尺寸可能始终是正方形,而不考虑方向。 - bojan

9

这是一个现代化的Combine解决方案:

import UIKit
import Combine

class MyClass: UIViewController {

     private var subscriptions = Set<AnyCancellable>()

     override func viewDidLoad() {
         super.viewDidLoad()
    
         NotificationCenter
             .default
             .publisher(for: UIDevice.orientationDidChangeNotification)
             .sink { [weak self] _ in
            
                 let orientation = UIDevice.current.orientation
                 print("Landscape: \(orientation.isLandscape)")
         }
         .store(in: &subscriptions)
    }
}

8

如果您使用的是 Swift 版本 >= 3.0,那么您需要应用一些代码更新,正如其他人已经提到的那样。只是不要忘记调用 super:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

   super.viewWillTransition(to: size, with: coordinator)

   // YOUR CODE OR FUNCTIONS CALL HERE

}

如果您想使用StackView来显示图片,请注意可以按照以下方式操作:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {

   super.viewWillTransition(to: size, with: coordinator)

   if UIDevice.current.orientation.isLandscape {

      stackView.axis = .horizontal

   } else {

      stackView.axis = .vertical

   } // else

}

如果您正在使用Interface Builder,请不要忘记在右侧面板的Identity Inspector部分中为此UIStackView对象选择自定义类。然后,只需通过Interface Builder创建(也可以)自定义UIStackView实例的IBOutlet引用即可:
@IBOutlet weak var stackView: MyStackView!

将这个想法适应于你的需求。希望这可以帮助到你!


调用super的好处很多。在重写函数时不调用super方法是一种非常粗糙的编码方式,它可能会导致应用程序产生不可预测的行为和难以跟踪的潜在错误! - Adam Freeman
你知道为什么我在子类化UIView时无法覆盖viewWillTransition方法吗? - Xcoder
@Xcoder,一个对象无法应用覆盖的原因(通常)在于运行时实例不是使用自定义类创建的,而是使用默认类创建的。如果您正在使用Interface Builder,请确保在右侧面板的Identity Inspector部分中选择此UIStackView对象的自定义类。 - WeiseRatel

8

Swift 4.2,RxSwift

如果我们需要重新加载collectionView。

NotificationCenter.default.rx.notification(UIDevice.orientationDidChangeNotification)
    .observeOn(MainScheduler.instance)
    .map { _ in }            
    .bind(to: collectionView.rx.reloadData)
    .disposed(by: bag)

Swift 4, RxSwift

如果我们需要重新加载collectionView。

NotificationCenter.default.rx.notification(NSNotification.Name.UIDeviceOrientationDidChange)
    .observeOn(MainScheduler.instance)
    .map { _ in }            
    .bind(to: collectionView.rx.reloadData)
    .disposed(by: bag)

6
我认为正确的答案实际上是两种方法的结合:viewWillTransition(toSize:)NotificationCenterUIDeviceOrientationDidChangeviewWillTransition(toSize:) 在转换之前向您发送通知。 NotificationCenterUIDeviceOrientationDidChange 在转换完成后向您发送通知。
您必须非常小心。例如,在 UISplitViewController 中,当设备旋转到某些方向时,DetailViewController 会从 UISplitViewControllerviewcontrollers 数组中弹出,并推送到主控制器的 UINavigationController 中。如果在旋转完成之前搜索详细视图控制器,则可能不存在并且会崩溃。

重写 willTransition(to newCollection: UITraitCollection) 也许是一个安全的尝试。 - aclima

5

Swift 4

在更新ViewControllers视图时,使用UIDevice.current.orientation可能会遇到一些小问题,例如在旋转期间更新tableview单元格的约束或子视图的动画。

相比上述方法,我目前正在将过渡大小与视图控制器视图大小进行比较。这似乎是正确的方法,因为此时代码可以访问两者:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    print("Will Transition to size \(size) from super view size \(self.view.frame.size)")

    if (size.width > self.view.frame.size.width) {
        print("Landscape")
    } else {
        print("Portrait")
    }

    if (size.width != self.view.frame.size.width) {
        // Reload TableView to update cell's constraints.
    // Ensuring no dequeued cells have old constraints.
        DispatchQueue.main.async {
            self.tableView.reloadData()
        }
    }


}

iPhone 6 的输出结果:

Will Transition to size (667.0, 375.0) from super view size (375.0, 667.0) 
Will Transition to size (375.0, 667.0) from super view size (667.0, 375.0)

我需要从项目级别打开方向支持吗? - Ericpoon

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