美文网首页码皇之路之 iOSiOS开发好文
JJStatusBarExtension 完美实现点击 stat

JJStatusBarExtension 完美实现点击 stat

作者: 德蒙_托尔斯泰 | 来源:发表于2016-02-24 23:43 被阅读270次

    背景

    • 现在有许多 app 都有这个需求, 点击 statusBar, tableView/collectionview 内容滚动到顶部
    • iOS其实已经集成了这种功能, 但是它只能在当前控制器之下只有一个(只能有一个) scrollView 或者其子类的时候才能有用, 如果你 tableView又有一个 scrollView 的标题栏, 那它自带的这个功能你是用不了的
    • 本文详细探求了如何扩展该功能, 使其能共通用
    • 代码详见本人 GitHub/quickCode/JJStatusBarExtension

    动手

    • 苹果是怎么实现该功能的, 我们无从得知, 所以我们得自己实现

    • 大致功能实现步骤: 点击 statusBar--> 触发手势-->-->找到我们需要操作的内容 view-->滚动它

      • 首先我们很容易想到, 在 statusBar 上盖上一个 view, 然后就监听点击就是了, 但我要告诉你这是不可取的
        • 你的 view 不可能盖到 statusBar 上, 因为它也是在一个独立的 window 上的, 这个 window 在我们 app.keyWindow之上, 所以你的 view 永远都会在 statusBar 下面(有兴趣的可以去打印看看)
        • 也许你会说把 view 加到了 statusBar 下也没关系, 把 statusBar 的点击忽略掉就行, 我只能说天真, 且不说 statusBar 层次结构复杂, 各种控件, 而且还拿不到这些控件, 就算能拿到, 你要在 hitTest 里面一步步递归判断吗, 这样不现实,所以否定这种想法
        • 加 view 不行, 那就只能加 window 了, 因为 Window是能加到 statusBar 上的, 只要改变 window 的优先级--windowLevel属性即可(补充一点, 优先级相同的 window, 后加的在上面)
      • 光加个 window 还不够, 我们需要一个顶层控制器来统一管理 statusBar 的, 这样才能在控制器里完成对 view 的点击操作进行监听, 光搞个 view 是没用的
        • window 多大合适, 直接告诉你, 和屏幕. bounds 相等即可, 为什么, 如果跟 statusBar.bounds 相等的话, 旋转屏幕的时候会有 bug, statusBar 会消失不见
        • 控制器 vi的大小就可以设置成 statusBar.bounds 大小了, 在 window 的 hitTest 方法里忽略掉 statusBar 以下的点击事件, 传递给 app.window处理
    • 主要代码:

      • 这里我把 topWindow 设计成单例, 方便后面拿到顶层的 topVc
    // JJStatusBarExtension.m
    static JJStatusBarExtension *_topWindow;
    
    + (instancetype)sharedStatusBarExtension
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _topWindow = [[self alloc] init];
        });
        return _topWindow;
    }
    
    + (instancetype)allocWithZone:(struct _NSZone *)zone
    {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            _topWindow = [super allocWithZone:zone];
        });
        return _topWindow;
    }
    
    - (id)copyWithZone:(NSZone *)zone
    {
        return _topWindow;
    }
    
    + (void)showWithStatusBarClickBlock:(void (^)())block
    {
        if (_topWindow) return;
        
        [JJStatusBarExtension sharedStatusBarExtension].windowLevel = UIWindowLevelAlert;
        [JJStatusBarExtension sharedStatusBarExtension].backgroundColor = [UIColor clearColor];
        // 先显示window
        [JJStatusBarExtension sharedStatusBarExtension].hidden = NO;
        
        // 设置根控制器
        JJTopViewController *topVc = [[JJTopViewController alloc] init];
        topVc.view.backgroundColor = [UIColor clearColor];
        topVc.view.frame = [UIApplication sharedApplication].statusBarFrame;
        topVc.view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
        topVc.clickedBlock = block;
        [JJStatusBarExtension sharedStatusBarExtension].rootViewController = topVc;
    }
    
    - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
    {
        if (point.y > 20) {
            return nil;
        }
        return [super hitTest:point withEvent:event];
    }
    
    • 通过 + showWithStatusBarClickBlock:方法来创建出 topWindow, 在 APPDelegate 中调用, 这里我把寻找内容 view 并滚动到最前面的方法独立出来, 并在 + showWithStatusBarClickBlock:后的 block 回调, 如果你其他地方有这个需求, 你可以使用这个方法, 你只要传入你要找的内容view 的父view 或父父view...
    • 这里我们把 application.keyWindow传进去, 因为我们需要全局实现
    • 实现滚动到 top 的原理, 递归查找子控件, 找到 scrollView 就把它滚到最前面
    // AppDelegate.m
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    //    NSLog(@"%@", self.window);
        [JJStatusBarExtension showWithStatusBarClickBlock:^{
    //        [self test];
            // 如果想要 app的所有界面都有点击 statusBar 滚到最前面, 则调用下面这个方法
            [JJStatusBarExtension scrollToTopInsideView:self.window];
        }];
        
        return YES;
    }
    
    + (void)scrollToTopInsideView:(UIView *)view
    {
        CGRect viewRect = [view convertRect:view.bounds toView:nil];
        if (!CGRectIntersectsRect([UIApplication sharedApplication].keyWindow.frame, viewRect)) {
            return;
        }
        
        for (UIView *subview in view.subviews) {
            [self scrollToTopInsideView:subview];
        }
        if (![view isKindOfClass:[UIScrollView class]]) {
            return;
        }
        UIScrollView *scrollView = (UIScrollView *)view;
        //    CGPoint contentOffset = scrollView.contentOffset;
        //    contentOffset.y = - scrollView.contentInset.top;
        //    [scrollView setContentOffset:contentOffset animated:YES];
        [scrollView scrollRectToVisible:CGRectMake(0, 0, 1, 1) animated:YES];
    }
    

    完成上面的控件部署, 下面就实现功能实现部分

    • 首先明白几个条件:
      • 以前可以用 application 来修改 statusBarHidden 和 style, 但 iOS9后, application 的方法过期了
      • 在控制器中, 控制器可以控制 statusBar 的 hidden 和 style, 首先明白一点, 系统是怎么来控制statusBar的, 有一点可以验证到, 就是每次 view 将要显示的时候, 系统都会调用下面的方法, 以此来控制 statusBar 状态
      • 通过setNeedsStatusBarAppearanceUpdate能重新调用下面的三个方法
      • statusBar 只能由顶层控制器来控制, 所以我们加了 topVc 后, 其他控制器里对 statusBar 状态的修改是无效的,因为顶层控制器会覆盖它
    • 根据上面的分析, 我们也在 view 将要显示的时候来操作, 在 viewWillAppear 方法来想办法
    - (BOOL)prefersStatusBarHidden
    {
        return YES;
    }
    - (UIStatusBarStyle)preferredStatusBarStyle
    {
        return UIStatusBarStyleDefault;
    }
    
    - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
    {
        return UIStatusBarAnimationFade;
    }
    
    • 为了保证系统原有的对 statusBar 的控制依然有效, 我们就必须在 topVc 里能
      拿到当前显示 Vc对 statusBar 的设置数据
    • 这里我们新建一个 viewController 的分类, 在这里面完成相关操作, 这里要弄到运行时的交换方法, 因为我们要拦截 viewController 的 viewWillAppear 方法
    • 拦截到后, 将当前显示控制器, 通过 topWindow 传给 topVc, 这样就能在 topVc 里面拿到当前显示控制器里对 statusBar 的舍子数据, 然后 topVc 在调用setNeedsStatusBarAppearanceUpdate方法, 刷新状态栏的显示
    • 这里还对如果设置了statusBar 显示动画的情况作了判断
    // UIViewController (JJStatusBarExtension) --分类
    @implementation UIViewController (JJStatusBarExtension)
    
    + (void)load
    {
      // 交换系统 viewWillAppear 和我们自定义的 jj_viewWillAppear
        Method method1 = class_getInstanceMethod(self, @selector(jj_viewWillAppear:));
        Method method2 = class_getInstanceMethod(self, @selector(viewWillAppear:));
        method_exchangeImplementations(method1, method2);
    }
    
    - (void)jj_viewWillAppear:(BOOL)animated
    {
        [self jj_viewWillAppear:animated];
        // 如果非当前窗口显示的控制器, 我只测试到UINavigationController下,"UIInputWindowController"会产生影响, 把它屏蔽掉
    //    NSLog(@"%@", self.class);
        if ([NSStringFromClass(self.class) isEqualToString:@"UIInputWindowController"]) return;
    
        if ([self respondsToSelector:@selector(jj_ignoreStatusBar)]) {
        if ([self jj_ignoreStatusBar]) return;
        }
    
        JJTopViewController *statusBarVc = (JJTopViewController *)[JJStatusBarExtension sharedStatusBarExtension].rootViewController;
        if (statusBarVc == self) return;
        statusBarVc.showingVc = self;
        // 判断是否设置动画
        if (statusBarVc.showingVc.preferredStatusBarUpdateAnimation == UIStatusBarAnimationNone) {
            [statusBarVc setNeedsStatusBarAppearanceUpdate];
        }else{
            [UIView animateWithDuration:[JJStatusBarExtension sharedStatusBarExtension].statusBarAnimationDuration animations:^{
                [statusBarVc setNeedsStatusBarAppearanceUpdate];
            }];
        }
    }
    @end
    
    • topVc拿到数据刷新,
      • topVc 的 touchsBegin中监听点击, 回调 scrollToTop 的方法
    @interface JJTopViewController : UIViewController
    @property(nonatomic, strong) void(^clickedBlock)();
    @property(nonatomic, strong) UIViewController * showingVc;
    @end
    
    @implementation JJTopViewController
    
    - (BOOL)prefersStatusBarHidden
    {
        return self.showingVc.prefersStatusBarHidden;
    }
    
    - (UIStatusBarStyle)preferredStatusBarStyle
    {
        return self.showingVc.preferredStatusBarStyle;
    }
    
    - (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
    {
        return self.showingVc.preferredStatusBarUpdateAnimation;
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        if (self.clickedBlock) {
            self.clickedBlock();
        }
    }
    
    @end
    

    以上就是所有实现的主要思路以及代码

    • 在此基础上, 还加了个jj_ignoreStatusBar的方法, 主要是防止当前显示 vc上还有多个小 vc的情况, 这时就在小 vc实现这个方法, 屏蔽小 vc 的影响
      • 解惑:因为jj_ignoreStatusBar这个方法我是不需要在我分类中实现的, 我只是需要在需要屏蔽的时候可选实现, 所以设计成这种协议模式, 这种设计模式, 大家可以借鉴借鉴, 其实我也不是理解的很深, 互相学习吧!

    以上就是所有内容了, 有什么不对的地方望请指出, 大家互相学习...有什么问题请留言

    相关文章

      网友评论

      本文标题:JJStatusBarExtension 完美实现点击 stat

      本文链接:https://www.haomeiwen.com/subject/qhyikttx.html