美文网首页
0代码解决个别界面横屏问题

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

作者: kagenZhao | 来源:发表于2019-11-09 14:46 被阅读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
}
  1. 在对应的Controller中重写shouldAutorotate, supportedInterfaceOrientations, preferredInterfaceOrientationForPresentation

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

二. 利用runtime && category

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

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

  • 首先, 对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
  1. 在pop的时候, 如果pop了超过一个controller, 那么也会出现pop回去不会旋转屏幕的问题, 同理用runtime处理一下pop方法, 利用viewWillAppear来解决这个问题


    pop_bug1

    解决代码如下:

+ (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 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_bug2
  1. 又产生了一个新的问题, 就是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

最后重复一下 项目地址
文章源地址请访问我的网站

撒花~~

相关文章

网友评论

      本文标题:0代码解决个别界面横屏问题

      本文链接:https://www.haomeiwen.com/subject/vlhnbctx.html