需求
目前iOS功能类app中, 大部分都是只针对竖屏编写的UI, 不过还有少数页面需要加入横屏, 比如横屏阅读文档、视频等. 这时候就需要针对这几个页面做横屏支持.
不看文章直接用? 点我直达github
解决方案
传统方案
这是目前网上比较流行的方案, 根据UI一层一层传递, 让Appdelegate知道当前页面是否需要横屏, 这里大多数人使用的是基类继承.
具体为:
创建
BaseViewController
,BaseTableViewController
,BaseNavigationController
, 并让App内部类都继承这三个基类, 并重写shouldAutorotate
,supportedInterfaceOrientations
,preferredInterfaceOrientationForPresentation
方法并返回对应最上层Controller的相同属性-
AppDelegate中实现协议方法, 并返回最上层界面的
supportedInterfaceOrientations
func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask { // 在这里返回(伪代码) // tabbar.topNavigation.topController.supportedInterfaceOrientations }
在对应的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中在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;
}
}
解决后:
坑2
又产生了一个新的问题, 就是pop的时候动画不是那么美观, 咱们放慢来看一下:
我这里的解决方案是在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];
}
结果如下:
结尾
目前这个库中就遇到这两个问题, 解决以后比较完美
其他
目前系统的类用还有一些类有时候不能旋转, 也可以通过注册一个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
最后重复一下 项目地址