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