UIStatusBar提示条的实现

在某一个大雪纷飞的下午,某汪找到某猿。

某汪:帮我做一个状态栏上的提示条呗。
某猿:那是系统的,苹果不准并且没有开放API。
某汪:我看某某APP就有的,再说这点小事能难倒你嘛!
某猿:当然不能,我并非浪得虚名的!

于是就有了下面的故事。

前期分析

众所周知,系统状态栏是通过一个windowLevelUIWindowLevelStatusBarUIWindow实现的。

  • UIApplication.h中可以找到UIStatusBarUIStatusBarWindow的类申明,然后就莫有了。然并卵!
  • 通过下面私有API能拿到当前APP的所有UIWindow, 参见here

[UIWindow allWindowsIncludingInternalWindows:YES onlyVisibleWindows:NO]

我们可以用一个windowLevel大于UIWindowLevelStatusBarUIWindow来挡住系统状态栏。这样就可以实现自己的状态栏提示条了。so easy。

构建自己的statusBarWindow

这里对每个iOS工程师来说都可以省略5千字,但是有2个坑需要注意。

  • 一个不是全屏的UIWindow处理转屏比较麻烦,最好把我们的statusBarWindow做成全屏,然后处理掉事件响应。 通过重写下面的系统API能很方便地处理事件响应。

- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;   // recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;   // default returns YES if point is in bounds

  • iOS8.3以后,在电话、导航、录音时,系统会把我们UIWindow的rootViewController.view下移20。可以通过下面代码把它改回来。

- (void)viewDidLayoutSubviews {
    [super viewDidLayoutSubviews];
    if (self.view.y == 20) {
        self.view.y = 0;
        self.view.height += 20;
    }
}

有了自己的statusBarWindow就万事大吉了嘛?答案肯定不是啊,是我还写什么。
UIStatusBar是不停改变的,一种通过UIApplication.h的API

- (void)setStatusBarHidden:(BOOL)hidden withAnimation:(UIStatusBarAnimation)animation
- (void)setStatusBarStyle:(UIStatusBarStyle)statusBarStyle animated:(BOOL)animated 
@property(readwrite, nonatomic) UIStatusBarStyle statusBarStyle

UIViewController.h中的相关API也能修改状态栏

另一种是因为来电、导航、录音等导致系统自己修改的。这种修改只会导致状态栏的底色发生改变,字体颜色默认都是白色。

对于前一种修改,我们可以通过hook住相关API来实现,对于运行时的方法hook有太多的教程和案例,这里就不叙述了。那对于系统自己修改的,我们怎么来处理呢?

获取UIStatusBar对象

我们知道UIStatusBar就是系统状态栏的具体UIView,先把它拿到。通过getTopView可以拿到UIStatusBar

- (UIView *)getTopView {
    NSString *result = [NSString stringWithFormat:@"%@%@%@",[self getUtilName1:YES], [self getUtilName2:YES],[self getUtilName3:YES]];
    id app = [UIApplication sharedApplication];
    id view = objc_msgSend(app, NSSelectorFromString(result));
    return view;
}

//下面几个方法获取我们需要的方法名
- (NSString *)getUtilName1:(BOOL)isTop {
    if (isTop) {
        return @"sta";
    } else {
        return @"k";
    }
}

- (NSString *)getUtilName2:(BOOL)isTop {
    if (isTop) {
        return @"tus";
    } else {
        return @"ey";
    }
}

- (NSString *)getUtilName3:(BOOL)isTop {
    if (isTop) {
        return @"Bar";
    } else {
        return @"Window";
    }
}

获取到具体的UIStatusBar对象之后,我们来看看系统到底是怎么实现来电、导航等状态栏改变的。

监听UIStatusBar底色改变

系统状态栏的底色是通过一个名为UIStatusBarBackgroundView的subview来反应的,文字部分主要通过一个名为UIStatusBarForegroundView的subview来反应。

我们使用Aspect来截获UIStatusBar- (void)didAddSubview:(UIView *)subview;。具体代码如下:

[[self getTopView] aspect_hookSelector:@selector(didAddSubview:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, UIView *subview){
        NSArray *subviews = [[aspectInfo instance] subviews];
        NSLog(@"didAddSubview, subviews : %@, subview : %@", subviews, subview);
    } error:NULL];

结果如下:

第一次断点:

第二次断点:

实践证明,底色的改变居然是通过subview来实现的,每当需要修改底色时都会有新的UIStatusBarBackgroundViewUIStatusBarForegroundView加到UIStatusBar中,同时移掉旧的subview.

好了,知道怎么回事,就可以来监听这个底色改变了。

- (void)initStatusBarData {
    //初始化状态栏状态
    [self topViewBackgroundColorDidChange];
    __weak typeof(self) weakSelf = self;
    [[self getTopView] aspect_hookSelector:@selector(willRemoveSubview:) withOptions:AspectPositionAfter usingBlock:^(id<AspectInfo> aspectInfo, UIView *subview) {
        NSArray *subviews = [[aspectInfo instance] subviews];
        //count的计算是因为启动本app之后再开始录音,则会出现一层UIView,subviews.count会为4
        int count = 0;
        for(UIView *view in subviews) {
            if ([view isKindOfClass:NSClassFromString(@"UIStatusBarForegroundView")] || [view isKindOfClass:NSClassFromString(@"UIStatusBarBackgroundView")]) {
                count++;
            }
        }
        if (count == 3) {
            [weakSelf topViewBackgroundColorDidChange];
        }
    } error:NULL];
}

- (void)topViewBackgroundColorDidChange {
    UIView *colorView = [self getTopColorView];
    UIColor *color = colorView.backgroundColor;
    if (color) {
        //状态栏此时有颜色
    } else {
        //使用UINavigationBar的颜色
    }
}

- (UIView *)getTopColorView {
    return [[[self getTopView] subviews] firstObject];
}

搞定收工!

本文来自网易实践者社区,经作者雷琨 授权发布。