JJStatusBarExtension 完美實現(xiàn)點擊 statusBar 滾動到頂部

背景

  • 現(xiàn)在有許多 app 都有這個需求, 點擊 statusBar, tableView/collectionview 內容滾動到頂部
  • iOS其實已經(jīng)集成了這種功能, 但是它只能在當前控制器之下只有一個(只能有一個) scrollView 或者其子類的時候才能有用, 如果你 tableView又有一個 scrollView 的標題欄, 那它自帶的這個功能你是用不了的
  • 本文詳細探求了如何擴展該功能, 使其能共通用
  • 代碼詳見本人 GitHub/quickCode/JJStatusBarExtension

動手

  • 蘋果是怎么實現(xiàn)該功能的, 我們無從得知, 所以我們得自己實現(xiàn)

  • 大致功能實現(xiàn)步驟: 點擊 statusBar--> 觸發(fā)手勢-->-->找到我們需要操作的內容 view-->滾動它

    • 首先我們很容易想到, 在 statusBar 上蓋上一個 view, 然后就監(jiān)聽點擊就是了, 但我要告訴你這是不可取的
      • 你的 view 不可能蓋到 statusBar 上, 因為它也是在一個獨立的 window 上的, 這個 window 在我們 app.keyWindow之上, 所以你的 view 永遠都會在 statusBar 下面(有興趣的可以去打印看看)
      • 也許你會說把 view 加到了 statusBar 下也沒關系, 把 statusBar 的點擊忽略掉就行, 我只能說天真, 且不說 statusBar 層次結構復雜, 各種控件, 而且還拿不到這些控件, 就算能拿到, 你要在 hitTest 里面一步步遞歸判斷嗎, 這樣不現(xiàn)實,所以否定這種想法
      • 加 view 不行, 那就只能加 window 了, 因為 Window是能加到 statusBar 上的, 只要改變 window 的優(yōu)先級--windowLevel屬性即可(補充一點, 優(yōu)先級相同的 window, 后加的在上面)
    • 光加個 window 還不夠, 我們需要一個頂層控制器來統(tǒng)一管理 statusBar 的, 這樣才能在控制器里完成對 view 的點擊操作進行監(jiān)聽, 光搞個 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:方法來創(chuàng)建出 topWindow, 在 APPDelegate 中調用, 這里我把尋找內容 view 并滾動到最前面的方法獨立出來, 并在 + showWithStatusBarClickBlock:后的 block 回調, 如果你其他地方有這個需求, 你可以使用這個方法, 你只要傳入你要找的內容view 的父view 或父父view...
  • 這里我們把 application.keyWindow傳進去, 因為我們需要全局實現(xiàn)
  • 實現(xiàn)滾動到 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];
}

完成上面的控件部署, 下面就實現(xiàn)功能實現(xiàn)部分

  • 首先明白幾個條件:
    • 以前可以用 application 來修改 statusBarHidden 和 style, 但 iOS9后, application 的方法過期了
    • 在控制器中, 控制器可以控制 statusBar 的 hidden 和 style, 首先明白一點, 系統(tǒng)是怎么來控制statusBar的, 有一點可以驗證到, 就是每次 view 將要顯示的時候, 系統(tǒng)都會調用下面的方法, 以此來控制 statusBar 狀態(tài)
    • 通過setNeedsStatusBarAppearanceUpdate能重新調用下面的三個方法
    • statusBar 只能由頂層控制器來控制, 所以我們加了 topVc 后, 其他控制器里對 statusBar 狀態(tài)的修改是無效的,因為頂層控制器會覆蓋它
  • 根據(jù)上面的分析, 我們也在 view 將要顯示的時候來操作, 在 viewWillAppear 方法來想辦法
- (BOOL)prefersStatusBarHidden
{
    return YES;
}
- (UIStatusBarStyle)preferredStatusBarStyle
{
    return UIStatusBarStyleDefault;
}

- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
{
    return UIStatusBarAnimationFade;
}
  • 為了保證系統(tǒng)原有的對 statusBar 的控制依然有效, 我們就必須在 topVc 里能
    拿到當前顯示 Vc對 statusBar 的設置數(shù)據(jù)
  • 這里我們新建一個 viewController 的分類, 在這里面完成相關操作, 這里要弄到運行時的交換方法, 因為我們要攔截 viewController 的 viewWillAppear 方法
  • 攔截到后, 將當前顯示控制器, 通過 topWindow 傳給 topVc, 這樣就能在 topVc 里面拿到當前顯示控制器里對 statusBar 的舍子數(shù)據(jù), 然后 topVc 在調用setNeedsStatusBarAppearanceUpdate方法, 刷新狀態(tài)欄的顯示
  • 這里還對如果設置了statusBar 顯示動畫的情況作了判斷
// UIViewController (JJStatusBarExtension) --分類
@implementation UIViewController (JJStatusBarExtension)

+ (void)load
{
  // 交換系統(tǒng) 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"會產(chǎn)生影響, 把它屏蔽掉
//    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拿到數(shù)據(jù)刷新,
    • topVc 的 touchsBegin中監(jiān)聽點擊, 回調 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

以上就是所有實現(xiàn)的主要思路以及代碼

  • 在此基礎上, 還加了個jj_ignoreStatusBar的方法, 主要是防止當前顯示 vc上還有多個小 vc的情況, 這時就在小 vc實現(xiàn)這個方法, 屏蔽小 vc 的影響
    • 解惑:因為jj_ignoreStatusBar這個方法我是不需要在我分類中實現(xiàn)的, 我只是需要在需要屏蔽的時候可選實現(xiàn), 所以設計成這種協(xié)議模式, 這種設計模式, 大家可以借鑒借鑒, 其實我也不是理解的很深, 互相學習吧!

以上就是所有內容了, 有什么不對的地方望請指出, 大家互相學習...有什么問題請留言

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容