iPhone中 rotation 处理

只支持某个页面旋转

最近有人问我一个关于iPhone上旋转的需求:App中只对某个页面支持旋转,其它页面一律只支持 portrait。比如,A push B,在B页面旋转后,页面会自适应;当B处于 landscape 时,pop回到A时,A还要处于 portrait。
一开始,我的答案时重载 - (UIInterfaceOrientationMask)supportedInterfaceOrientations,对方试了一下没有效果。亲身实践了一下,单设置此方法确实不行。了解到对方的项目中页面并没有完全使用 AutoLayout,但还是实现了 rotation 相关的方法,于是使用在 AppDelegate 中实现 - (UIInterfaceOrientationMask)application:(UIApplication *)application supportedInterfaceOrientationsForWindow:(UIWindow *)window,具体步骤是在 AppDelegate 中添加属性 UIInterfaceOrientationMask,在每个 VC 出现和消失的时候,更新该属性。回到这个例子,可以在 AppDelegate 初始化 UIInterfaceOrientationMaskUIInterfaceOrientationMaskPortrait,接着再在B中更新:

- (void)viewWillAppear:(BOOL)animated{
    //rotation support
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    [delegate updateSupportedOrientation:UIInterfaceOrientationMaskAllButUpsideDown];
}
- (void)viewWillDisappear:(BOOL)animated{
    //rotation support
    AppDelegate *delegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
    [delegate updateSupportedOrientation:UIInterfaceOrientationMaskPortrait];
}

这个方法的原理在于旋转的时候,UIWindow首先接收到相应,然后再传递给 rootViewController 以及子 viewController。而如果实现了协议方法 UIInterfaceOrientationMaskAllButUpsideDown:supportedInterfaceOrientationsForWindow:,那么每次系统检测到旋转事件后,都会调用该方法。

正常的旋转处理流程

iOS8开始,和 rotation 相关的几个方法已经废弃:

- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation;  
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;  
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration;  
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation;  

尤其是第一个,在编译阶段 Xcode 会提示该方法已经废弃。其它3个方法,如果没有使用新的处理rotation的方法时,还会继续执行。
如今,推荐使用的处理 rotation 的方法如下:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator;

使用coordinator可以处理不同方向下的constraints或是页面布局,这里主要注意的是如何正确的判断当前所处的模式。Apple推荐的是使用 UITraitCollection / UITraitEnvironment 来检测当前的方向,下面举个例子:

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: { context in
            self.p_updateContstraints()
        }) { contenxt in

        } 
    }

    fileprivate func p_updateContstraints() {
        let constraints = self.viewRed.constraints
        self.viewRed.removeConstraints(constraints)
        self.p_removeAnchorConstranits(target: self.viewRed, from: self.view)

        let widthConstraint = NSLayoutConstraint(item: self.viewRed!, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .width, multiplier: 1.0, constant: 50)
        let heightConstraint = NSLayoutConstraint(item: self.viewRed!, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .height, multiplier: 1.0, constant: 50)
        self.viewRed.addConstraints([widthConstraint, heightConstraint])

        let trait = self.traitCollection
        let safeLayoutGuide = self.view.safeAreaLayoutGuide
        if trait.userInterfaceIdiom == .phone {
            if trait.verticalSizeClass == .compact && trait.horizontalSizeClass == .regular{
                //plus landscape: left-top
                self.viewRed.topAnchor.constraint(equalTo: safeLayoutGuide.topAnchor, constant: 0).isActive = true
                self.viewRed.leadingAnchor.constraint(equalTo: safeLayoutGuide.leadingAnchor, constant: 0).isActive = true
            } else if trait.verticalSizeClass == .compact && trait.horizontalSizeClass == .compact {
                //landscape: left-bottom
                self.viewRed.bottomAnchor.constraint(equalTo: safeLayoutGuide.bottomAnchor, constant: 0).isActive = true
                self.viewRed.leadingAnchor.constraint(equalTo: safeLayoutGuide.leadingAnchor, constant: 0).isActive = true
            } else if trait.verticalSizeClass == .regular && trait.horizontalSizeClass == .compact {
                //portrait: right-top
                self.viewRed.topAnchor.constraint(equalTo: safeLayoutGuide.topAnchor, constant: 0).isActive = true
                self.viewRed.trailingAnchor.constraint(equalTo: safeLayoutGuide.trailingAnchor, constant: 0).isActive = true
            }
        }else if trait.userInterfaceIdiom == .pad{

        }else {

        }
    }
    fileprivate func p_removeAnchorConstranits(target: UIView, from superView: UIView ){
        let sConstraints = superView.constraints
        var toDeleteConstraints:[NSLayoutConstraint] = []
        for constraint in sConstraints {
            let firstView = constraint.firstItem as? UIView
            let secondView = constraint.secondItem as? UIView
            if firstView == target || secondView == target {
                toDeleteConstraints.append(constraint)
            }
        }
        superView.removeConstraints(toDeleteConstraints)
    }

上述代码主要是在iPhone portrait/landscape和plus landscape模式下,使红色方块处于不同的位置。因为使用了 safearealayoutguide,只对iOS11有效。

参考

uiviewcontroller
Preventing a View From Rotating
Size Class
判断方向

UIInterfaceOrientation Starting in iOS 8, you should employ the UITraitCollection and UITraitEnvironment APIs, and size class properties as used in those APIs, instead of using UIInterfaceOrientation constants or otherwise writing your app in terms of interface orientation. In earlier versions of iOS, you used these constants in the statusBarOrientation property and the setStatusBarOrientation:animated: method. Notice that UIDeviceOrientationLandscapeRightis assigned to UIInterfaceOrientationLandscapeLeft and UIDeviceOrientationLandscapeLeft is assigned to UIInterfaceOrientationLandscapeRight; the reason for this is that rotating the device requires rotating the content in the opposite direction.

willRotateToInterfaceOrientation等3个方法已经废弃