一、前言
在iOS開發(fā)中,我們常常看到好多應(yīng)用都有這樣一個(gè)效果,如果一個(gè)應(yīng)用當(dāng)前頁是是一個(gè)滾動(dòng)視圖,當(dāng)用戶點(diǎn)擊狀態(tài)欄的時(shí)候,滾動(dòng)視圖會(huì)自動(dòng)的返回內(nèi)容的最頂部。
其實(shí)這個(gè)功能是UIScrollView自帶的,不需要我們手動(dòng)去實(shí)現(xiàn),只需要設(shè)置self.scrollView.scrollsToTop = YES即可(scrollsToTop默認(rèn)為YES)。但是這個(gè)屬性有一個(gè)前提是窗口下必須只有一個(gè)可滾動(dòng)的View才有效果。
// When the user taps the status bar, the scroll view beneath the touch which is closest to the status bar will be scrolled to top, but only if its `scrollsToTop` property is YES, its delegate does not return NO from `shouldScrollViewScrollToTop`, and it is not already at the top.
// On iPhone, we execute this gesture only if there's one on-screen scroll view with `scrollsToTop` == YES. If more than one is found, none will be scrolled.
在實(shí)際應(yīng)用中,我們可能會(huì)有多個(gè)scrollView(包含UITableView/UICollectionView),如網(wǎng)易新聞、簡書、愛奇藝,騰訊視頻等等應(yīng)用,這時(shí)候,系統(tǒng)默認(rèn)的點(diǎn)擊狀態(tài)欄返回到頂部效果就會(huì)失效,原因是當(dāng)前window下多個(gè)scrollView的scrollsToTop屬性的值都是YES,系統(tǒng)不知道該讓哪一個(gè)scrollView滾動(dòng)到頂部,所以就什么也不干了。如果還要讓當(dāng)前視圖滾回頂部,只需要將除了當(dāng)前顯示的其它scrollView的scrollsToTop屬性設(shè)置成NO就行。
但是今天要講的是應(yīng)簡友要求,講一下自定義控件來實(shí)現(xiàn)此功能。就是《LYTopWindow》實(shí)現(xiàn)原理。
二、基本思路
1.獲取當(dāng)狀態(tài)欄的點(diǎn)擊事件
2.取得當(dāng)前window上顯示的scrollView
3.控制scrollView的偏移量,讓其滾動(dòng)到頂部
三、實(shí)現(xiàn)步驟
新建一個(gè)空白項(xiàng)目,我們可以查看應(yīng)用結(jié)構(gòu)可以知道,狀態(tài)欄是獨(dú)立出來的,并不在window上

所以我們不能拿到狀態(tài)欄直接操作,那么就需要自定義一個(gè)控件蓋在狀態(tài)欄上面,用來攔截狀態(tài)欄的點(diǎn)擊事件。
一、添加窗口
由于添加普通的控件都不能蓋在狀態(tài)欄上面,所有要新建一個(gè)普通的window(keyWindow只能有一個(gè))蓋到狀態(tài)欄的上面。
- 在
AppDelegate創(chuàng)建一個(gè)新的窗口,從iOS9.0開始,程序啟動(dòng)結(jié)束時(shí)的窗口都必須設(shè)置rootViewController。否則會(huì)報(bào)以下錯(cuò)誤:
Application windows are expected to have a root view controller at the end of application launch
這里可以通過dispatch_after來給添加窗口一個(gè)延時(shí)就可以不設(shè)置根控制器而不會(huì)報(bào)錯(cuò)
-
UIWindow的顯示不需要添加到其它控件上,只需要將hidden設(shè)置成NO就行。 - 要想讓自定義窗口蓋在狀態(tài)欄的上面還要設(shè)置窗口的級別
windowLevel,窗口的級別有三種,分別是UIWindowLevelAlert > UIWindowLevelStatusBar > UIWindowLevelNormal,級別越高就越顯示在頂部,如果級別一樣,那么后創(chuàng)建添加的顯示在頂部。
static UIWindow *topWindow_;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
topWindow_ = [[UIWindow alloc] init];
topWindow_.frame = [UIApplication sharedApplication].statusBarFrame;
topWindow_.backgroundColor = [UIColor clearColor];
topWindow_.hidden = NO;
topWindow_.windowLevel = UIWindowLevelAlert;
});
return YES;
}
然后在給topWindow添加一個(gè)手勢監(jiān)聽點(diǎn)擊事件就可以攔截到狀態(tài)欄的點(diǎn)擊事件了。
但是,這樣寫會(huì)有一個(gè)Bug,如果旋轉(zhuǎn)屏幕的話,window上的添加的View的frame不會(huì)跟著屏幕旋轉(zhuǎn)而改變,就會(huì)不正確。
- 如果需要屏幕旋轉(zhuǎn)的話,必須要給
UIWindwo設(shè)置rootViewController; - Bug產(chǎn)生的原因是
Autoresizing的影響,在旋轉(zhuǎn)的屏幕時(shí),窗口的View寬高被拉伸而造成frame不正確,這時(shí)只需要設(shè)置一下View跟隨窗口的變化而變化即可。
topWindow_.rootViewController.view.autoresizingMask = UIViewAutoresizingFlexibleWidth;
居然這里已經(jīng)設(shè)置了rootViewController,那么就不需要dispatch_after。
static UIWindow *topWindow_;
-(BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
topWindow_ = [[UIWindow alloc] init];
topWindow_.frame = [UIApplication sharedApplication].statusBarFrame;
topWindow_.backgroundColor = [UIColor clearColor];
topWindow_.hidden = NO;
topWindow_.windowLevel = UIWindowLevelAlert;
[topWindow_ addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(topWindowClick)]];
return YES;
}
二、監(jiān)聽頂部窗口點(diǎn)擊,實(shí)現(xiàn)當(dāng)前滾動(dòng)視圖滾回頂部
/** * 監(jiān)聽頂部窗口點(diǎn)擊 */
+(void)topWindowClick{
UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow;
[self searchAllScrollViewsInView:keyWindow];
}
查找當(dāng)前keyWindow上的滾動(dòng)視圖,如果跟keyWindow重疊,就讓其滾動(dòng)到頂部
/**
* 查找view中的所有scrollView
*/
-(void)searchAllScrollViewsInView:(UIView *)view {
// 如果不在UIWindow矩形框里面,就直接返回
// view和UIWindow沒有重疊,就直接返回
if (![view intersectWithView:nil]) return;
for (UIView *subview in view.subviews) {
[self searchAllScrollViewsInView:subview];
}
// 如果不是UIScrollView, 直接返回
if (![view isKindOfClass:[UIScrollView class]]) return;
UIScrollView *scrollView = (UIScrollView *)view;
// 讓UIScrollView滾動(dòng)到最前面
CGPoint offset = scrollView.contentOffset;
offset.y = - scrollView.contentInset.top;
[scrollView setContentOffset:offset animated:YES];
}
LYTopWindow的基本實(shí)現(xiàn)原理就是這樣了,如果想看了解基本實(shí)現(xiàn)步驟可以查看LYTopWindow的源代碼:https://github.com/DeveloperLY/LYTopWindow 。
三、補(bǔ)充
如果使用了自定義控件實(shí)現(xiàn)了點(diǎn)擊狀態(tài)欄滾動(dòng)視圖自動(dòng)滾回頂部,那么控制器的這兩個(gè)方法會(huì)失效
1.控制器的- (BOOL)prefersStatusBarHidden方法決定狀態(tài)欄的可見性
2.控制器的- (UIStatusBarStyle)preferredStatusBarStyle方法決定狀態(tài)欄的樣式
原因就是狀態(tài)欄的樣式\可見性,由最頂層(蓋在最上面)window的控制器來決定
所以如果使用了LYTopWindwo框架的,如果要控制狀態(tài)欄的樣式和可見性可以使用下面的代碼:
// 可見性
[LYTopWindow sharedTopWindow].statusBarHidden = NO;
[LYTopWindow sharedTopWindow].statusBarHidden = YES;
// 樣式
[LYTopWindow sharedTopWindow].statusBarStyle = UIStatusBarStyleDefault;
[LYTopWindow sharedTopWindow].statusBarStyle = UIStatusBarStyleLightContent;