碰到的问题
最近在适配UI的时候,发现在iOS13
及以后的版本进入一个视频播放页面后,调用更改状态栏的API
无效了。后面发现是由于iOS13
以后对Scene
的适配以及一个隐藏的UIWindow
导致的,iOS13
加入Scene
后,系统状态栏的实现发现了很大的变化。这里对iOS
状态栏进行一个系统的总结备后续参考。
如何控制状态栏
使用UIApplication
控制(iOS7之前)
在iOS7
之前,一般都是通过直接使用UIApplication
来变更状态栏的样式和隐藏状态。
1 2 3 4 5 6
| @available(iOS, introduced: 2.0, deprecated: 9.0, message: "Use -[UIViewController preferredStatusBarStyle]") open func setStatusBarStyle(_ statusBarStyle: UIStatusBarStyle, animated: Bool)
@available(iOS, introduced: 3.2, deprecated: 9.0, message: "Use -[UIViewController prefersStatusBarHidden]") open func setStatusBarHidden(_ hidden: Bool, with animation: UIStatusBarAnimation)
|
我们可以直接通过调用:UIApplication.shared.setStatusBarStyle(.lightContent, animated: true)
来修改状态栏,非常的简单粗暴。但是在复杂逻辑中,这样全局的设置容易造成混乱和不好维护。所以在iOS7
后提供了新的控制体系,并把上述方法在iOS9
标记为deprecated
。
View controller-based status bar appearance
方式(iOS7之后)
Apple
在iOS7
中提供了新的状态栏控制方法,由VC
去负责自己生命周期中的状态栏控制。使用新方法首先要在Info.plist
中将View controller-based status bar appearance
设为YES
。
1 2 3 4
| ... <key>UIViewControllerBasedStatusBarAppearance</key> <true/> ...
|
然后在相应的VC
中重写两个控制方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| func changeStatusBar() { statusBarStyle = .lightContent isStatusBarHidden = true setNeedsStatusBarAppearanceUpdate() }
override var preferredStatusBarStyle: UIStatusBarStyle { get { return statusBarStyle } }
public override var prefersStatusBarHidden: Bool { get { return isStatusBarHidden } }
|
通过上述代码,我们就可以通过VC
来控制当前它所在视图的状态栏的样式。但是在实际使用中并没有这样简单,因为VC
可能是嵌套在UINavigationController
中,或者我需要VC
的childViewControllers
来控制状态栏的样式等。这就涉及到控制权限问题了。
子控制器的控制权限问题
这种类型非常简单,如果你想要使用子控制器来更改状态栏的状态,重写UIViewController
的childForStatusBarStyle
方法即可:
1 2 3 4 5 6 7 8 9 10 11
| override var childForStatusBarStyle: UIViewController? { let childVC = children[0] return childVC }
override var childForStatusBarHidden: UIViewController? { let childVC = children[0] return childVC }
|
这样状态栏的控制权限就交给了子控制器childVC
。
UINavigationController
的控制权限问题
这种情况涉及到导航栏,会复杂一些,分为状态栏是否可见两种场景,它的规则如下:
- 当
NavigationBar
不可见时,由它栈顶的topViewController
控制状态栏。
- 当
NavigationBar
可见时,由它自身控制状态栏。这种情况下,由它的barStyle
属性决定状态栏的样式:
- 当
barStyle
= .default
时,NavigationBar
显示为白色,此时StatusBar
显示为黑色
- 当
barStyle
= .black
时,NavigationBar
显示为白色,此时StatusBar
显示为白色
如果你既不想隐藏状态栏,又想让topViewController
来控制状态栏,那么可以像上面子控制器的控制权限问题
的方法一样,重写childForStatusBarStyle
和childForStatusBarHidden
即可。
Modal Presentation
的控制权限问题
当我们presnet
一个VC
时,它的状态栏由谁控制,取决于它是否是全屏展示。现在VC
默认的modalPresentationStyle
是UIModalPresentationStyle.automatic
,并不是全屏展示,所以默认情况下它自身并不控制状态栏。但是当我们将modalPresentationStyle
改为UIModalPresentationStyle.fullScreen
后,它变成了全屏展示,就获得了状态栏的控制权。
如果既不想让present
的VC
全屏显示,又想让它控制状态栏,那么将它的modalPresentationCapturesStatusBarAppearance
属性设置为true
即可。
iOS13之前状态栏的底层实现及状态栏无法控制的场景
在iOS13
之前,状态栏是通过UIWindow
来实现的,可以很方便的通过UIApplication
拿到状态栏对应的视图。
以前还有通过状态栏获取网络状态等骚操作。
在iOS13
之前,我们可以通过如下代码来分析状态栏:
1 2 3 4 5 6 7 8
| id bar = [[UIApplication sharedApplication] valueForKey:@"_statusBar"]; NSLog(@"class:%@, superClass:%@", [bar class], [bar superclass]); NSLog(@"superview:%@", [bar superview]);
|
从上述样例可以看到,状态栏的类名叫UIStatusBar
,它继承自UIStatusBar_Base
,被添加在UIStatusBarWindow
(UIWindow的子类)上被显示出来。
当我们新建一个UIWindow
,将它隐藏,它是不会影响到keyWindow
的状态栏控制的,但是当将它设为不隐藏并且将frame
的值设为UIScreen.main.bounds
的时候,keyWindow
的状态栏控制将会失效。有趣的是,将frame
改小或改大后,都不会导致失效,只有值为UIScreen.main.bounds
的时候才会导致异常,感觉像是iOS的源码里面有代码逻辑来判断frame
是否与UIScreen.main.bounds
相等一样。
windowLevel
不影响上述结论
iOS13之后状态栏的底层实现及状态栏无法控制的场景
iOS13
之后,状态栏的底层实现完全发生了变化,从UIApplication
取_statusBar
只会返回nil
,对UIStatusBar
,UIStatusBar_Base
,UIStatusBarWindow
等类的初始化方法hook
,发现都没有被调用。这说明iOS13之后,状态栏的视图实现逻辑被完全重构了。
iOS13新增了一个UIStatusBarManager
类(之前也有,但是是私有的),系统源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| @available(iOS 13.0, *) open class UIStatusBarManager : NSObject { open var statusBarStyle: UIStatusBarStyle { get }
open var isStatusBarHidden: Bool { get }
open var statusBarFrame: CGRect { get } }
extension UIWindowScene { @available(iOS 13.0, *) open var statusBarManager: UIStatusBarManager? { get } }
|
通过runtime
打印出它的成员变量和方法列表分别如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
| // 成员变量列表 "_overriddingStatusBarHidden" = "@"; "_scene" = UIScene; "_statusBarFrameIgnoringVisibility" = "GRect={CGPoint=dd}{CGSize=dd}"; debugDescription = NSString; debugMenuHandler = "@"; defaultStatusBarHeight = "@"; description = NSString; hash = "@"; inStatusBarFadeAnimation = "@"; localStatusBars = NSMutableSet; statusBarAlpha = "@"; statusBarFrame = "GRect={CGPoint=dd}{CGSize=dd}"; statusBarHeight = "@"; statusBarHidden = "@"; statusBarPartStyles = NSDictionary; statusBarStyle = "@"; superclass = "@"; windowScene = UIWindowScene;
// 方法列表 .cxx_destruct _settingsDiffActionsForScene: initWithScene: _scene _setScene: windowScene isStatusBarHidden defaultStatusBarHeightInOrientation: statusBarStyle statusBarHeight setWindowScene: updateStatusBarAppearance updateLocalStatusBars statusBarHidden statusBarAlpha setupForSingleLocalStatusBar updateStatusBarAppearanceWithAnimationParameters: statusBarFrameForStatusBarHeight: defaultStatusBarHeight _updateStatusBarAppearanceWithClientSettings:transitionContext:animationParameters: _updateVisibilityForWindow:targetOrientation:animationParameters: _updateStyleForWindow:animationParameters: _updateAlpha _visibilityChangedWithOriginalOrientation:targetOrientation:animationParameters: activateLocalStatusBar: _updateLocalStatusBar: statusBarFrame _handleScrollToTopAtXPosition: _adjustedLocationForXPosition: _setOverridingStatusBarHidden: _setOverridingStatusBarHidden:animationParameters: statusBarFrameForStatusBarHeight:inOrientation: _statusBarFrameIgnoringVisibility updateStatusBarAppearanceWithClientSettings:transitionContext: deactivateLocalStatusBar: createLocalStatusBar handleTapAction: _isOverridingStatusBarHidden localStatusBars setLocalStatusBars: statusBarPartStyles isInStatusBarFadeAnimation debugMenuHandler setDebugMenuHandler:
|
通过runtime
调用createLocalStatusBar
创建状态栏发现状态栏的类名是:_UIStatusBarLocalView
,对这个类及它相关属性进行分析,然后进行hook,发现这些类运行时都没有创建和使用,线索到这里就断了。
虽然底层实现发生了变化,但是状态栏状态变更的接口和结果依然和之前版本保持一致,上面说的导致keyWindow
的状态栏控制失效的场景也依然存在。但是我在实际开发中碰到了一个更诡异的问题,在我们现在的项目中,需要适配CarPlay,所以使用了Scene
,使用Scene
后,所有的UIWindow
必须设置它的windowScene
属性,它才能正确显示在对应的scene
中,由于项目中引用了DoraemonKit
等第三方框架,它们内部使用UIWindow
进行显示,为了让项目正常运行,同事hook了一些代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @implementation UIView (SceneHook) + (void)load { if (@available(iOS 13.0, *)) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL selector = @selector(initWithFrame:); Method method = class_getInstanceMethod(self, selector); if (!method) { NSAssert(NO, @"Method not found for [UIView initWithFrame:]"); } IMP imp = method_getImplementation(method); class_replaceMethod(self, selector, imp_implementationWithBlock(^(UIView *self, CGRect frame) { ((UIView * (*)(UIView *, SEL, CGRect))imp)(self, selector, frame); if ([self isKindOfClass:UIWindow.class]) { UIWindowScene *scene = [UIApplication sharedApplication].keyWindow.windowScene; [(UIWindow *)self setWindowScene:scene]; } return self; }), method_getTypeEncoding(method)); }); } }
@end
|
我们有接入腾讯的视频播放SDK,它在播放视频时会创建一个window
单例:SuperPlayerWindow
,用于实现播放器小窗功能,这个window
默认是隐藏的,在iOS13
之前没有问题,但是在iOS13
及之后会导致keyWindow
中VC
的状态栏控制功能失效。(注意这个window是隐藏不显示的)
吐槽下,整一个视图单例,创建后就在app生命周期中一直存在,这种设计明显是不合理的。
将这个SuperPlayerWindow
的frame
设为UIScreen.main.bounds
之外的值可以解决这个问题,将上述hook代码进行如下修改也可以解决这个问题:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @implementation UIWindow (SceneHook) + (void)load { if (@available(iOS 13.0, *)) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SEL selector = @selector(initWithFrame:); Method method = class_getInstanceMethod(self, selector); if (!method) { NSAssert(NO, @"Method not found for [UIWindow initWithFrame:]"); } IMP imp = method_getImplementation(method); class_replaceMethod(self, selector, imp_implementationWithBlock(^(UIWindow *self, CGRect frame) { ((UIWindow * (*)(UIWindow *, SEL, CGRect))imp)(self, selector, frame); UIWindowScene *scene = [UIApplication sharedApplication].keyWindow.windowScene; [(UIWindow *)self setWindowScene:scene]; return self; }), method_getTypeEncoding(method)); }); } }
@end
|
上述两段代码没有看到和状态栏有什么关联的地方,但是却实实在在的产生影响,受制于iOS
的UIKit
是闭源的,没法通过源码查找它的产生的原因,如果你有什么思路,欢迎留言讨论~
参考资料