屏幕自动旋转与手动旋转总结
source link: https://hanleylee.com/articles/auto-rotate-of-ios/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
iOS 的屏幕旋转有很多弯弯绕的地方, 旋转的成功与否取决于多个层面的共同作用.
横竖屏设置的地方
AppDelegate.swift
// 自定义属性, 用于控制全局旋转方向 var interfaceOrientations: UIInterfaceOrientationMask = .portrait func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { return interfaceOrientations }
Info.plist
VC.swift
/// 自动旋转的变量 (用于设置只读属性 shouldAutoratate) private var shouldAutorotateVariable = false private var supportOrientations: UIInterfaceOrientationMask = [.landscapeLeft, .landscapeLeft, .portrait] // 是否支持自动转屏 override open var shouldAutorotate: Bool { get { return shouldAutorotateVariable } set { shouldAutorotateVariable = newValue } } // 本 vc 所支持的转动方向 override var supportedInterfaceOrientations: UIInterfaceOrientationMask { get { return supportOrientations } set { supportOrientations = newValue } } override var preferredInterfaceOrientationForPresentation: UIInterfaceOrientation { return .landscapeRight }
shouldAutoratate 与旋转级别
要理解各个 VC
的控制关系, 首先要理清最重要的 VC 中的 shouldAutoratate
属性的设置
如果某个
VC
中的shouldAutorotate
被设置为false
, 那么系统将忽略下面的设置:UIApplicationDelegate
中的supportedInterfaceOrientationsForWindow:
方法- 该
VC
通过supportedInterfaceOrientations
方法设置的自己支持的屏幕方向
系统只考虑用户在 “
Info.plist
中的设置.如果某个
VC
中的shouldAutorotate
未被重写 (或者被重写为true
, 因为默认值就是true
), 那么系统将优先考虑使用下面两个设置的交集:UIApplicationDelegate
中的supportedInterfaceOrientationsForWindow
: 方法 (默认不实现, 即不支持任何方向)- 该
VC
通过supportedInterfaceOrientations
方法设置的自己支持的屏幕方向 (默认不实现, 即支持任何方向)
如果以上两个设置只有其中一种, 就按照他们的默认行为来进行判断; 如果没有以上两个设置, 再使用 “
Info.plist
中的设置. 因此, 我们有时可能会有一种UIApplicationDelegate
中的方法会覆写Info.plist
设置的错觉切记注意: 如果两个方法都设置了且没有任何交集, 那么有崩溃的风险!
shouldAutoratate
为什么不走正常情况下屏幕旋转时会访问
VC
中的shouldAutorotate
属性, 断点会走这里; 但是很多人都会发现这个断点不走这个属性了, 问题的根源就在于继承上:- 如果不在根控制器 (
UITabBarController
和UINavigationController
) 中设置那 3 个方法,VC
中重写shouldAutorotate
是不会被调用的. - 如果在根控制器中设置了那三个方法, 那么第一个控制器的
shouldAutorotate
能调用, 但是往下push
的控制器中的shouldAutorotate
就不调用了, 不管勾选没勾选横屏. - 如果
modal
一个没有实现上面三个方法的UINavigationController
控制器, 那么新控制器的shouldAutorotate
也是不能调用 - 如果
modal
一个普通的viewController
, 控制器中的shouldAutorotate
能被调用 - 如果
modal
一个自定义转场的控制器, 不能调用
- 如果不在根控制器 (
监听一定要监听
UIApplication.didChangeStatusOrientationNotification
必须要监听
UIApplication.didChangeStatusOrientationNotification
而不是UIDevice.orientationDidChangeNotification
, 原因有下:UIDevice
的通知会在从后台进入前台时调用三次, 具体原因不明UIDevice
会有多个方向的通知, 比如从faceUp
转向landscapeLeft
时也会发出一个通知, 但这是我们一般是不需要这个通知的
一定要使用
UIApplication.shared.statusBarOrientation
来判断方向我们也可以使用
UIDevice.current.orientation
来判断当前设备的方向, 这个方向的类别是UIDeviceOrientation
, 其一共有如下可能:- unknown
- portrait
- portraitUpsideDown
- landscapeLeft
- landscapeRight
- faceUp
- faceDown
这非常不利于我们对方向的判断, 我们只想要当前设备的屏幕方向到底是横向还是竖向, 因此使用
UIApplication.shared.statusBarOrientation
是最好的选择, 他的类别是UIInterfaceOrientationMask
, 共有如下可能:- portrait
- landscapeLeft
- landscapeRight
- portraitUpsideDown
- landscape
- allButUpsideDown
屏幕旋转的重新约束
如果 UI 元素使用了 autoLayout, 那么在屏幕旋转后其布局依然是遵守 autoLayout 的, 但是如果是使用 frame 的绝对布局, 那么一定要重新进行布局, 可以在 VC 的
layoutSubviews
或 view 的viewWillLayoutSubviews
方法中根据横竖屏判断进行重新布局, 也可以在旋转监听方法中写布局改变的代码
屏幕事件处理代码示例
let appdelegate = UIApplication.shared.delegate as! AppDelegate
// 监听屏幕旋转事件
NotificationCenter.default.rx.notification(UIApplication.didChangeStatusBarOrientationNotification)
.subscribe(onNext: { [weak self] (_) in
guard let self = self, UIViewController.topMost == self else {return} // 控制仅最前端为自己时才会触发改变元素方法
self.changeElementsWhenRotate(showLock: true)
})
.disposed(by: disposeBag)
/// 显示横屏与竖屏的逻辑 (添加移除相关 view)
/// - Parameter showLock: 是否显示锁定界面, 因为要隐藏 tabbar, 进入个股详情后再退回到本页面会自动显示底部 tabBar,
/// 因此要在 viewWillAppear 中调用本方法, 并且不显示锁定框
private func changeElementsWhenRotate(showLock: Bool) {
guard let wtVC = vcDic[.wt] as? QTWTMainVC else { return }
wtVC.popView.removeFromeSuperview()
wtVC.menuBtn.tintColor = UIColor(hex: 0xB3B3B3)
/// 菜单栏高度
var menuHeight: CGFloat = 0.0
/// 跑马灯高度
var marqueHeight: CGFloat = 0.0
if UIApplication.shared.statusBarOrientation.isLandscape {
self.lockToastView.isHidden = !showLock
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {
self.lockToastView.isHidden = true
}
navigationController?.setNavigationBarHidden(true, animated: false)
unlockBtn.isHidden = false
self.tabBarController?.tabBar.isHidden = true
wtVC.noLoginView.isHidden = true
menuHeight = 0.0
wtVC.menuBtn.isHidden = true
marqueHeight = 0.0
} else {
navigationController?.setNavigationBarHidden(false, animated: false)
self.lockToastView.isHidden = true
self.lockSuccessView.isHidden = true
unlockBtn.isHidden = true
tabBarController?.tabBar.isHidden = false
wtVC.noLoginView.isHidden = userM.isLogon ? true : false
wtVC.menuBtn.isHidden = false
menuHeight = 40.0
marqueHeight = 30.0
}
wtVC.menuView.snp.remakeConstraints { make in
make.top.equalToSuperview()
make.left.equalToSuperview().offset(13)
make.right.equalToSuperview().offset(-80.0)
make.height.equalTo(menuHeight)
}
/// 若存在跑马灯, 则将跑马灯高度设为零并且本身 view 距顶部设为零
if let parent = parent as? BaseMarqueeController, let marque = parent.marqueeView {
marqueHeight = (marque.isRunning || marque.isPaused) ? marqueHeight : 0.0 // 在跑马灯运行期间, 仍然使用跑马灯自身的高度 30, 否则为 0
marque.isHidden = marqueHeight == 0.0 ? true : false // 本步骤隐藏之后即可不使用下一步骤的重新布局, 加上是双重保险
marque.snp.remakeConstraints {(make) in
make.top.left.right.equalToSuperview()
make.height.equalTo(marqueHeight)
}
self.view.snp.remakeConstraints {(make) in
make.top.equalTo(marqueHeight)
make.left.right.bottom.equalToSuperview()
}
}
subviews.enumerated().forEach { (index, view) in
view.snp.remakeConstraints {(make) in
make.top.height.width.equalToSuperview()
make.left.equalToSuperview().offset(UIDevice.ck.width * CGFloat(index))
}
}
// 保证在旋转屏幕后得到正确的子 VC 布局
wtVC.containerVcs.forEach {(type, vc) in
vc.view.snp.remakeConstraints {(make) in
make.left.equalTo(UIDevice.ck.width * CGFloat(type.index))
make.top.equalToSuperview()
make.width.equalTo(UIDevice.ck.width)
make.height.equalToSuperview()
}
}
scrollView.setContentOffset(CGPoint(x: self.scrollView.bounds.width * CGFloat(quotationType.index), y: 0), animated: false)
wtVC.scrollView.setContentOffset(CGPoint(x: CGFloat(wtVC.selectedVCType.index) * UIDevice.ck.width, y: 0), animated: false)
}
// 强制旋转屏幕
unlockBtn.rx.tap
.subscribe(onNext: { [weak self] (_) in
guard let self = self else {return}
self.unlockBtn.isHidden = true
self.appdelegate.interfaceOrientations = .all
self.isLocked = false
UIDevice.current.setValue(UIInterfaceOrientation.portrait.rawValue, forKey: "orientation")
if UIDevice.current.orientation == .portrait { // 特殊情况: 锁定后在竖屏状态下解锁
UIViewController.attemptRotationToDeviceOrientation()
self.showLandscape()
}
})
.disposed(by: disposeBag)
headerView.buttonRotate.rx.tap
.subscribe(onNext: { (_) in
UIDevice.current.setValue(UIInterfaceOrientation.landscapeRight.rawValue, forKey: "orientation")
UIViewController.attemptRotationToDeviceOrientation()
})
.disposed(by: disposeBag)
// 强制锁定屏幕
lockSuccessView.tapGesture.rx.event
.subscribe(onNext: { [weak self] (_) in
guard let self = self else {return}
self.lockSuccessView.isHidden = true
switch UIDevice.current.orientation {
case .landscapeLeft: self.appdelegate.interfaceOrientations = .landscapeRight
case .landscapeRight: self.appdelegate.interfaceOrientations = .landscapeLeft
default: break
}
})
.disposed(by: disposeBag)
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK