Make writing a habit together! This is my first day to participate in the “Gold Digging Day New Plan · April More text challenge”, click to see the details of the activity.

Problems encountered

A recent UI adaptation found that the API for calling the change status bar did not work when entering a video playback page in iOS13 or later. The later discovery is caused by iOS13’s adaptation of the Scene and a hidden UIWindow. After iOS13 added the Scene, the implementation of the system status bar found a big change. Here is a summary of the iOS status bar system for subsequent reference.

How do I control the status bar

useUIApplicationControls (before iOS7)

Before iOS7, it was common to change the style of the status bar and hide the state directly using UIApplication.

//UIApplication
@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)
Copy the code

We can directly by calling: UIApplication. Shared. SetStatusBarStyle (. LightContent, animated: true) to modify the status bar, very simple and crude. But in complex logic, such a global setup can be confusing and difficult to maintain. So in iOS7, the new control system was introduced, and the above method was marked deprecated in iOS9.

View controller-based status bar appearanceMode (after iOS7)

Apple in iOS7 provides a new status bar control method, by VC to be responsible for their own life cycle status bar control. To use the new method, first set the View Controller-based Status bar appearance to YES in info.plist.

. <key>UIViewControllerBasedStatusBarAppearance</key> <true/>
...
Copy the code

Then rewrite two control methods in the corresponding VC:

func changeStatusBar(a) {
    statusBarStyle = .lightContent
    isStatusBarHidden = true
    // The status bar is refreshed only after the call
    setNeedsStatusBarAppearanceUpdate()
}

// Control the status bar style
override var preferredStatusBarStyle: UIStatusBarStyle {
    get {
        return statusBarStyle
    }
}

// Control the status bar to hide the status
public override var prefersStatusBarHidden: Bool {
    get {
        return isStatusBarHidden
    }
}
Copy the code

With the above code, we can use VC to control the style of the status bar of the view in which it is currently located. But in practice it’s not that simple, because the VC may be nested in the UINavigationController, or I need the VC childViewControllers to control the style of the status bar, etc. This involves the issue of control authority.

The control permission of the child controller is incorrect

This type is very simple. If you want to use child controllers to change the state of the status bar, override the childForStatusBarStyle method on UIViewController:

override var childForStatusBarStyle: UIViewController? {
    let childVC = children[0]
    // Give control to childVC
    return childVC
}

override var childForStatusBarHidden: UIViewController? {
    let childVC = children[0]
    // Give control to childVC
    return childVC
}
Copy the code

This gives control of the status bar to the child controller childVC.

UINavigationControllerThe control permission of the

This case involves the navigation bar, which is more complex and can be divided into two scenarios: whether the status bar is visible or not. Its rules are as follows:

  1. whenNavigationBarInvisible when it’s on top of the stacktopViewControllerControl status bar.
  2. whenNavigationBarWhen visible, it controls the status bar itself. In this case, by itsbarStyleProperty determines the style of the status bar:
    • whenbarStyle = .defaultWhen,NavigationBarIs displayed as white, at this timeStatusBarIt’s black
    • whenbarStyle = .blackWhen,NavigationBarIs displayed as white, at this timeStatusBarIt’s white

If you don’t want to hide the status bar and you want topViewController to control the status bar, you can override childForStatusBarStyle and childForStatusBarHidden in the same way that the upper face controller controls permissions.

Modal PresentationThe control permission of the

When we presnet a VC, who controls its status bar depends on whether it is displayed in full screen. Now, VC is the default modalPresentationStyle UIModalPresentationStyle automatic, not full screen display, so by default it does not control the status bar. But when we will modalPresentationStyle UIModalPresentationStyle instead. After fullScreen, it turned into a full screen display, won the status bar control.

If don’t want to let the present VC full-screen, and want to let it control status bar, then its modalPresentationCapturesStatusBarAppearance attribute set to true.

IOS13 status bar before the underlying implementation and status bar control scenarios

Before iOS13, the status bar was implemented through UIWindow, so it’s very easy to get the view of the status bar through UIApplication.

In the past, there were operations such as obtaining network status through the status bar.

Before iOS13, we could analyze the status bar with the following code:

// Get the status bar
id bar = [[UIApplication sharedApplication] valueForKey:@"_statusBar"];
NSLog(@"class:%@, superClass:%@", [bar class], [bar superclass]);
NSLog(@"superview:%@", [bar superview]);
/* Demo[28973:959757] class:UIStatusBar, superClass:UIStatusBar_Base Demo[28973:959757] superView:
      
       ; layer = 
       
        > */
       
      
Copy the code

As you can see from the above sample, the status bar class is called UIStatusBar, which inherits from UIStatusBar_Base and is added to UIStatusBarWindow (a subclass of UIWindow) to be displayed.

When we create a UIWindow and hide it, it does not affect the state bar control of the keyWindow, but when we set it to unhidden and set the frame value to uiscreen.main. bounds, the state bar control of the keyWindow is disabled. Interestingly, making a frame smaller or larger does not invalidate the frame, but only if it is uiscreen.main. bounds. It feels like there is code logic in the iOS source code to determine whether the frame is equal to uiscreen.main. bounds.

WindowLevel does not affect the above conclusions

IOS13 status bar after the underlying implementation and status bar can not control the scene

After iOS13, the underlying implementation of the status bar has completely changed, so fetching _statusBar from UIApplication only returns nil, so I hook the initialization methods of UIStatusBar, UIStatusBar_Base, UIStatusBarWindow, None of them were called. This shows that after iOS13, the view implementation logic of the status bar has been completely refactored.

Although the underlying implementation has changed, the interface and result of status bar state changes remain the same as in previous versions, and the situation that caused keyWindow’s status bar control to fail still exists. In our current project, we need to adapt CarPlay, so we use Scene. After using Scene, all UIWindow must set its windowScene property so that it can be correctly displayed in the corresponding Scene. Due to the reference of DoraemonKit and other third-party frameworks in the project, they use UIWindow to display internally. In order to make the project run normally, my colleagues hook some codes:

@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
Copy the code

We have the video playback SDK connected to Tencent, which creates a Window singleton when playing the video: SuperPlayerWindow, used to implement the player window function, this window is hidden by default, iOS13 is not a problem, but iOS13 and later will cause the VC status bar control function in keyWindow to be disabled. (Note that the window is hidden.)

Ironically, an entire view singleton, once created, persists throughout the app’s life cycle, which is obviously unreasonable.

Setting the SuperPlayerWindow frame to a value outside the bounds of uiscreen.main. bounds solves this problem, as does modifying the above hook code as follows:

@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
Copy the code

The above two code does not see what is associated with the status bar, but it has a real impact, due to iOS UIKit is closed source, can not find the cause of its generation through the source code, if you have any ideas, welcome to comment ~

The resources

  • UIStatusBarStyle to reassure
  • Manage the iOS status bar
  • IOS13 gets the StatusBar and gets the network status
  • New change in Xcode11: SceneDelegate
  • IOS13 Scene Delegate