0代码解决个别界面横屏问题

需求

目前iOS功能类app中, 大部分都是只针对竖屏编写的UI, 不过还有少数页面需要加入横屏, 比如横屏阅读文档、视频等. 这时候就需要针对这几个页面做横屏支持.

不看文章直接用? 点我直达github

解决方案

传统方案

这是目前网上比较流行的方案, 根据UI一层一层传递, 让Appdelegate知道当前页面是否需要横屏, 这里大多数人使用的是基类继承.

具体为:

  1. 创建BaseViewController, BaseTableViewController, BaseNavigationController, 并让App内部类都继承这三个基类, 并重写shouldAutorotate, supportedInterfaceOrientations, preferredInterfaceOrientationForPresentation方法并返回对应最上层Controller的相同属性

  2. AppDelegate中实现协议方法, 并返回最上层界面的supportedInterfaceOrientations

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
        // 在这里返回(伪代码) 
        // tabbar.topNavigation.topController.supportedInterfaceOrientations
    }
    
  3. 在对应的Controller中重写shouldAutorotate, supportedInterfaceOrientations, preferredInterfaceOrientationForPresentation

在这个方案中最大的问题就是对项目的侵入性较大, 使项目的耦合性增大

利用runtime && category

作为iOS开发对runtime肯定都很了解, 并且应该知道category是可以覆盖原类方法的, 我正是利用这一点. 下边简述了该步骤, 代码部分不再赘述

这里要注意一点!!!, 代码要OC的, 因为Swift不支持category覆盖原有类的方法

最开始的方案是依赖Category覆盖系统类方法来实现的, 从Xcode 11 开始遇到一个问题: iOS13 在 debug连接模拟器/真机调试时无法触发Category重写系统的一些列方法(但不影响release包), 故从3.0.0版本开始完全使用Swizzle的形式实现功能, 与下面文章原理类似, 只是把category改成了Swizzle

首先, 对UITabBarController, UINavigationController, UIViewController分别实现category 并重写shouldAutorotate, supportedInterfaceOrientations, preferredInterfaceOrientationForPresentation, 重写的目的是设置默认值, 然后根据递归返回最上层的Controller的三个方法, 如果对应类中没有重写则默认设置不支持横屏

- (BOOL)shouldAutorotate {
    UIViewController *topVC = self.rotation_findTopViewController;
    return topVC == self ? defaultShouldAutorotate : topVC.shouldAutorotate;
  }

- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
    UIViewController *topVC = self.rotation_findTopViewController;
    return topVC == self ? defaultSupportedInterfaceOrientations : topVC.supportedInterfaceOrientations;
  }

- (UIInterfaceOrientation)preferredInterfaceOrientationForPresentation {
    UIViewController *topVC = self.rotation_findTopViewController;
    return topVC == self ? defaultPreferredInterfaceOrientationForPresentation : topVC.preferredInterfaceOrientationForPresentation;
}

然后, 利用runtime替换AppDelegate中的application:supportedInterfaceOrientationsForWindow:, 返回UIInterfaceOrientationMaskAll.

至于为什么返回UIInterfaceOrientationMaskAll, 因为如果当present出来的controller返回的旋转方向不包含在application代理之内的话, 会引起崩溃.

这样就完成了最简单的配置工作, 把这个category拖进项目里, 只需要重写对应Controller的三个方法就能让这个界面支持横屏

遇到的坑

在实现方案二的同时, 也遇到了两个坑, 这里跟大家分享一下.

坑1

UINavigationController进行push的时候, 默认是不会调用push出来的controller的方法的, 这里就需要用runtime重写navigation的push, 和controller的viewWillAppear来解决:

ps: 这个项目里用到的所有runtime方法是基于RSSwizzle的, 不过因为这个库有一个小bug没有解决, 所以我把他的两个文件放到了自己的项目里, 并且解决了bug, 替换了所有方法名和类名. 不用担心会冲突.

push_bug1

在push中在viewWillAppear中计算是否需要旋转屏幕 然后强制进行屏幕旋转

+ (void)rotation_hook_push {
    [KZRSSwizzle
    swizzleInstanceMethod:@selector(pushViewController:animated:)
    inClass:UINavigationController.class
    newImpFactory:^id(KZRSSwizzleInfo *swizzleInfo) {
        void (*originalImplementation_)(__unsafe_unretained id, SEL, UIViewController *viewController, BOOL animated);
        SEL selector_ = @selector(pushViewController:animated:);
        return ^void (__unsafe_unretained id self, UIViewController *viewController, BOOL animated) {
            UIViewController *fromViewController = [self viewControllers].lastObject;
            UIViewController *toViewController = viewController;
            [self rotation_setupPrientationWithFromVC:fromViewController toVC:toViewController];
            KZRSSWCallOriginal(viewController, animated);
        };
    }
    mode:KZRSSwizzleModeAlways
    key:NULL];
}

- (void)rotation_setupPrientationWithFromVC:(UIViewController *)fromViewController toVC:(UIViewController *)toViewController {
   if ([toViewController supportedInterfaceOrientations] & (1 << fromViewController.rotation_fix_preferredInterfaceOrientationForPresentation)) {
       toViewController.rotation_viewWillAppearBlock = nil;
       return;
   }
       if (fromViewController.rotation_fix_preferredInterfaceOrientationForPresentation != toViewController.rotation_fix_preferredInterfaceOrientationForPresentation) {
       __weak __typeof(toViewController) weakToViewController = toViewController;
       __weak __typeof(self) weakSelf = self;
       toViewController.rotation_viewWillAppearBlock = ^{
           __strong __typeof(weakToViewController) toViewController = weakToViewController;
           __strong __typeof(weakSelf) strongSelf = weakSelf;
           if (toViewController == nil) { return; }
           UIInterfaceOrientation ori = toViewController.rotation_fix_preferredInterfaceOrientationForPresentation;
           [strongSelf rotation_forceToOrientation:ori];
       };
   } else {
       toViewController.rotation_viewWillAppearBlock = nil;
   }
}

解决后:

push_no_bug

坑2

又产生了一个新的问题, 就是pop的时候动画不是那么美观, 咱们放慢来看一下:

pop_bug2_slow

我这里的解决方案是在pop的时候如果超过一个, 则在中间插入一个正向的临时controller

在上一个代码的基础上修改成下面代码:

+ (void)rotation_hook_popToRoot {
    [KZRSSwizzle
     swizzleInstanceMethod:@selector(popToRootViewControllerAnimated:)
     inClass:UINavigationController.class
     newImpFactory:^id(KZRSSwizzleInfo *swizzleInfo) {
         NSArray<UIViewController *> *(*originalImplementation_)(__unsafe_unretained id, SEL, BOOL animated);
         SEL selector_ = @selector(popToRootViewControllerAnimated:);
         return ^NSArray<UIViewController *> * (__unsafe_unretained id self, BOOL animated) {
             if ([self viewControllers].count < 2) { return nil; }
             UIViewController *fromViewController = [self viewControllers].lastObject;
             UIViewController *toViewController = [self viewControllers].firstObject;
             if ([fromViewController rotation_fix_preferredInterfaceOrientationForPresentation] == [toViewController rotation_fix_preferredInterfaceOrientationForPresentation]) {
                 return KZRSSWCallOriginal(animated);
             }
             /////////////////////////// 新增代码
             if ([toViewController rotation_fix_preferredInterfaceOrientationForPresentation] == UIInterfaceOrientationPortrait) {
                 NSMutableArray<UIViewController *> * vcs = [[self viewControllers] mutableCopy];
                 InterfaceOrientationController *fixController = [[InterfaceOrientationController alloc] initWithRotation:(UIDeviceOrientation)UIInterfaceOrientationPortrait];
                 fixController.view.backgroundColor = [toViewController.view backgroundColor];
                 [vcs insertObject:fixController atIndex:vcs.count - 1];
                 [self setViewControllers:vcs];
                 return [@[[self popViewControllerAnimated:true]] arrayByAddingObjectsFromArray:KZRSSWCallOriginal(false)];
             }
             /////////////////////////// 新增代码结束
             if ([toViewController supportedInterfaceOrientations] & (1 << fromViewController.rotation_fix_preferredInterfaceOrientationForPresentation)) {
                 return KZRSSWCallOriginal(animated);
             }
             __weak __typeof(toViewController) weakToViewController = toViewController;
             toViewController.rotation_viewWillAppearBlock = ^{
                 __strong __typeof(weakToViewController) toViewController = weakToViewController;
                 if (toViewController == nil) { return; }
                 UIInterfaceOrientation ori = toViewController.rotation_fix_preferredInterfaceOrientationForPresentation;
                 [toViewController rotation_forceToOrientation:ori];
                 toViewController.rotation_viewWillAppearBlock = nil;
             };
             return KZRSSWCallOriginal(animated);
         };
     }
     mode:KZRSSwizzleModeAlways
     key:NULL];
}

结果如下:

pop_no_bug2

结尾

目前这个库中就遇到这两个问题, 解决以后比较完美

其他

目前系统的类用还有一些类有时候不能旋转, 也可以通过注册一个model来让他强制支持旋转.比如这几个:

static inline NSArray <UIViewControllerRotationModel *> * __UIViewControllerDefaultRotationClasses() {
    NSArray <NSString *>*classNames = @[
    @"AVPlayerViewController",
    @"AVFullScreenViewController",
    @"AVFullScreenPlaybackControlsViewController",
    @"WebFullScreenVideoRootViewController",
    @"UISnapshotModalViewController",
    ];
    NSMutableArray <UIViewControllerRotationModel *> * result = [NSMutableArray arrayWithCapacity:classNames.count];
    [classNames enumerateObjectsUsingBlock:^(NSString * _Nonnull className, NSUInteger idx, BOOL * _Nonnull stop) {
        [result addObject:[[[[UIViewControllerRotationModel alloc]
                             initWithClass:className
                             containsSubClass:YES]
                            configShouldAutorotate:true]
                           configSupportedInterfaceOrientations:UIInterfaceOrientationMaskAll]];
    }];
    return result;
}

真.结尾

目前的功能就是这些, 如果有其他需求请添加Issues

最后重复一下 项目地址