背景
- 現(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處理
- 首先我們很容易想到, 在 statusBar 上蓋上一個 view, 然后就監(jiān)聽點擊就是了, 但我要告訴你這是不可取的
-
主要代碼:
- 這里我把 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é)議模式, 這種設計模式, 大家可以借鑒借鑒, 其實我也不是理解的很深, 互相學習吧!