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

最后重复一下 项目地址

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342