iOS 屏幕旋转的那些事(一)

前言

本文将讨论的话题是:iOS 屏幕旋转的那些事,文中示例 Demo 请参见:https://github.com/tangqi92/QTHandleOrientationChanges.git


需求

在即将推出的花瓣 4.0 版本中我们增加了对 iPad 横屏的支持,因为在实践的过程中踩了些坑,所以在此作下总结,同时也希望能帮助到有需要的朋友们。

由于历史原因,项目是手撕 Frame 而不是 Storyboard 进行布局,也并没有使用 Auto Layout。

又由于历史原因,项目设置为 「Universal」以支持 iPad。虽然支持 iPad,不过仅支持竖屏,其实也就是个放大的 iPhone 版,体验效果不佳。这次花瓣 4.0 的改版,iPad 横屏的支持便是一重点改进点,效果可见下图:

如果你是使用 Storyboard 进行布局,当然可以通过 Size Classes 来解决多尺寸设备适配的问题。不过前面已经提到,本人喜欢手写布局,但依然可以实现效果。

PS:如果你对「布局实现方式的选择」或「Auto Layout」等概念仍有疑问,你可阅读下面参考文章,本文就不作过多详述:

  1. Understanding Auto Layout
  2. Auto Layout 设计美学
  3. iOS 开发中,搭建界面的一些争论
  4. WWDC2016 Session笔记 - Xcode 8 Auto Layout新特性

实现

因为当屏幕旋转的时候,Controller 的 View 的 Size 会发生改变,就会调用 viewWillLayoutSubviews() 等方法重新布局,如果你是在这些方法里面布局的话,那么界面中的内容会重新布局,更进一步,如果你是在这些方法里面使用 Auto Layout 布局,那么就不需要再做额外的处理了。

但是,如果你是在 viewDidLoad() 等里面布局,那么界面中的内容将不会变化,你可能就需要监听屏幕旋转的地方重新布局,处理好旋转动画等。

于是,实现的关键便是处理屏幕的旋转事件,手动进行布局修改,以适配横竖屏不同效果的布局。那,我们又是如何知道设备的方向发生了改变的呢?不妨看看下面这些方法吧:

1
2
3
4
5
6
7
8
9
10
11
@property(nonatomic,readonly) UIInterfaceOrientation interfaceOrientation NS_DEPRECATED_IOS(2_0,8_0) __TVOS_PROHIBITED;

// Notifies when rotation begins, reaches halfway point and ends.
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration NS_DEPRECATED_IOS(2_0,8_0, "Implement viewWillTransitionToSize:withTransitionCoordinator: instead") __TVOS_PROHIBITED;
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation NS_DEPRECATED_IOS(2_0,8_0) __TVOS_PROHIBITED;

- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration NS_DEPRECATED_IOS(3_0,8_0, "Implement viewWillTransitionToSize:withTransitionCoordinator: instead") __TVOS_PROHIBITED;

- (void)willAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration NS_DEPRECATED_IOS(2_0, 5_0) __TVOS_PROHIBITED;
- (void)didAnimateFirstHalfOfRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation NS_DEPRECATED_IOS(2_0, 5_0) __TVOS_PROHIBITED; // The rotating header and footer views are offscreen.
- (void)willAnimateSecondHalfOfRotationFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation duration:(NSTimeInterval)duration NS_DEPRECATED_IOS(2_0, 5_0) __TVOS_PROHIBITED; // A this point, our view orientation is set to the new orientation.

别高兴的太早,网上大部分文章(至少中文文章)都会告诉你使用上面的这些方法,而然,Apple 早已在 iOS 8.0 中便将这些关于 Controller 的 Rotation APIs 进行废弃,取而代之的是推荐使用:

1
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator NS_AVAILABLE_IOS(8_0);

即当 Controller 的 View 的 Size 发生改变时便会触发该方法,下面来看下官方文档的解释:

其中 Size 为 View 改变(旋转)后的 Size,而 coordinator 则可用来处理转换动画。例如,你可以在该方法中改变子 View 的位置或大小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {

[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];

// Code here will execute before the rotation begins.
// Equivalent to placing it in the deprecated method -[willRotateToInterfaceOrientation:duration:].
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
// Place code here to perform animations during the rotation.
// You can pass nil for this closure if not necessary.
// Reorganize views, or move child view controllers.
if (UIInterfaceOrientationIsLandscape([UIApplication sharedApplication].statusBarOrientation)) {
[self.redView mas_updateConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(200, 200));
}];
}

if (UIInterfaceOrientationIsPortrait([UIApplication sharedApplication].statusBarOrientation)) {
[self.redView mas_updateConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(100, 100));
}];
}
}
completion:^(id<UIViewControllerTransitionCoordinatorContext> context){
// Code here will execute after the rotation has finished.
// Equivalent to placing it in the deprecated method -[didRotateFromInterfaceOrientation:].
// Do any cleanup, if necessary.

}];
}

注释非常简单,就不做翻译了,其中,你可以通过 [UIApplication sharedApplication].statusBarOrientation) 来获取当前设备的方向,便于你之后的操作。

嗯,这篇文章很水,而我会在下篇文章:iOS 屏幕旋转的那些事(二) 中具体介绍我遇到的一个坑。

我知道是不会有人点的,但万一有人想不开呢?