解決點擊狀態欄時ScrollView自動滾動到初始位置失效辦法
來自: http://www.jianshu.com/p/836cdd481982
相信細心的開發者都會發現scrollView自帶一個功能,當用戶點擊頂部的狀態欄時,scrollView的ContentOffset.y軸會自動滾動到初始位置,效果如圖所示:

單個scrollView單擊頂部狀態欄系統自帶功能展示
這個功能對用戶來說非常實用,尤其是在scrollView( TableView, WebView, CollectionView一切繼承scrollView的控件 )展示的內容很多,當用戶翻看很久以后,想回到最頂部時,只需單擊一下頂部的狀態欄位置就可以輕松返回到頂部( 這里吐槽下.貌似很多用戶都不知道有這個功能 ),而不用使勁用手滑動到頂部.
可是功能在當前控制器有多個scrollView( TableView, WebView, CollectionView一切繼承scrollView的控件 )的時候就會失效,效果如下圖所示:

當控制器內有多個scrollView時,系統自帶的滾動到頂的功能就會失效
- 如圖所示,一旦有多個scrollView時,系統自帶的方法就失效了
實際開發中,我們的產品在同一個控制器經常會有多個scrollView組合在一起的情況,這就意味著系統的方法已經失效了,需要開發人員自己來實現這個效果,下面我們就來搞定這個需求
我們分析下原生的方法為什么會失效,當一個控制器內只有一個scrollView時,點擊狀態欄,系統會遍歷當前keyWindow的子控件,發現子控件中只有一個scrollView會調用這個scrollView的setContentOffset: animated:的這個方法,將scrollView的contentOffset.y值修改為初始值,但是當子控件中又多個scrollView時,系統會不知道掉用哪一個scrollView而失效,知道這點我們就知道該如何搞定這個問題了
這里就直接將解決思路一一寫出來不將代碼分段展示了,在代碼中我加了詳細的注釋 objective-c的套路和swift基本一樣 ,在最后會將Swift和objective-c的代碼一起放上,如果需要直接解決問題的童鞋可以直接將代碼拷貝到工程里即可
- 首先創建一個topWindow繼承至NSObject,這里我們考慮將這個功能完全封裝起來,所以所有的方法都用的類方法,所以用最基本的類就可以
- 在initialize方法中初始化topWIndow,將topWIndow的級別改成最高的UIWindowLevelAlert級別,設置topWindow位置,并且添加點擊手勢
- 在topWIndow被點擊調用的方法中,我們拿出UIApplication的keyWindow,遍歷keyWindow的所有子控件,如果滿足是scrollView同時又顯示在當前keyWindow條件時,將subView的contentOffset的y值回復到原始
- 然后采用遞歸的套路在遍歷subView內時候有滿足條件的子控件,直到沒有滿足條件時會停止
Swift的代碼
import UIKitclass TopWindow: UIWindow {
private static let window_: UIWindow = UIWindow() /// 類初始化方法,保證window_只被創建一次 override class func initialize() { window_.frame = CGRectMake(0, 0, global.appWidth, 20) window_.windowLevel = UIWindowLevelAlert window_.addGestureRecognizer(UITapGestureRecognizer(target: self, action: "topWindowClick")) } class func topWindowClick() { // 遍歷當前主窗口所有view,將滿足條件的scrollView滾動回原位 searchAllowScrollViewInView(UIApplication.sharedApplication().keyWindow!) } private class func searchAllowScrollViewInView(superView: UIView) { for subview: UIView in superView.subviews as! [UIView] { if subview.isKindOfClass(UIScrollView.self) && superView.viewIsInKeyWindow() { // 拿到scrollView的contentOffset var offest = (subview as! UIScrollView).contentOffset // 將offest的y軸還原成最開始的值 offest.y = -(subview as! UIScrollView).contentInset.top // 重新設置scrollView的內容 (subview as! UIScrollView).setContentOffset(offest, animated: true) } // 遞歸,讓子控件再次調用這個方法判斷時候還有滿足條件的view searchAllowScrollViewInView(subview) } } /// 添加topWindow,使手勢生效 class func showTopWindow() { window_.hidden = false } /// 隱藏topWindow,移除手勢 class func hiddenTopWindow() { window_.hidden = true }
}
/// 對UIView的一個擴展 extension UIView {
/// 判斷調用方法的view是否在keyWindow中 func viewIsInKeyWindow() -> Bool { let keyWindow = UIApplication.sharedApplication().keyWindow! // 將當前view的坐標系轉換到window.bounds let viewNewFrame = keyWindow.convertRect(self.frame, fromView: self.superview) let keyWindowBounds = keyWindow.bounds // 判斷當前view是否在keyWindow的范圍內 let isIntersects = CGRectIntersectsRect(viewNewFrame, keyWindowBounds) // 判斷是否滿足所有條件 return !self.hidden && self.alpha > 0.01 && self.window == keyWindow && isIntersects }
}</pre>
-
在AppDelegate里,程序啟動完成方法時添加就OK了
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
// 添加頂部的window TopWindow.showTopWindow() return true
}</pre> </li>
- 需要注意添加了自定義的window后,控制器的改變狀態欄狀態方法會失效,可以在info.plist中將改變狀態欄的管理權交給UIApplication解決,或者在需要改變狀態欄的控制器中調用 TopWindow.hiddenTopWindow() 即可,或者直接改info.plist,用 UIApplication.sharedApplication().setStatusBarStyle來管理
-
.h文件只暴露顯示和隱藏方法
#import <Foundation/Foundation.h> @interface WNXTopWindow : NSObject + (void)show; + (void)hide; @end
-
.m文件
#import "WNXTopWindow.h" @implementation WNXTopWindow static UIWindow *window_; //初始化window + (void)initialize { window_ = [[UIWindow alloc] init]; window_.frame = CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 20); window_.windowLevel = UIWindowLevelAlert; [window_ addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(windowClick)]]; } + (void)show { window_.hidden = NO; } + (void)hide { window_.hidden = YES; } // 監聽窗口點擊 + (void)windowClick { UIWindow *window = [UIApplication sharedApplication].keyWindow; [self searchScrollViewInView:window]; } + (void)searchScrollViewInView:(UIView *)superview { for (UIScrollView *subview in superview.subviews) { // 如果是scrollview, 滾動最頂部 if ([subview isKindOfClass:[UIScrollView class]] && [self isShowingOnKeyWindow: subView]) { CGPoint offset = subview.contentOffset; offset.y = - subview.contentInset.top; [subview setContentOffset:offset animated:YES]; } // 遞歸繼續查找子控件 [self searchScrollViewInView:subview]; } } + (BOOL)isShowingOnKeyWindow:(UIView *)view { // 主窗口 UIWindow *keyWindow = [UIApplication sharedApplication].keyWindow; // 以主窗口左上角為坐標原點, 計算self的矩形框 CGRect newFrame = [keyWindow convertRect:view.frame fromView:view.superview]; CGRect winBounds = keyWindow.bounds; // 主窗口的bounds 和 self的矩形框 是否有重疊 BOOL intersects = CGRectIntersectsRect(newFrame, winBounds); return !view.isHidden && view.alpha > 0.01 && view.window == keyWindow && intersects; } @end
-
同樣,也是在程序初始化完成AppDelegate文件中顯示topWindow,整個工程這個問題就統統解決了
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 添加一個window, 點擊這個window, 可以讓屏幕上的scrollView滾到最頂部 [WNXTopWindow show]; return YES; }
針對iOS9.0會運行程序崩潰的解決辦法
- 更新了下Xcode7.0,發現運行程序會崩潰,解決方法是給頂部的topWindow添加一個rootViewController,創建一個topVC繼承至UIViewController,將topVC設置為頂部自定義的window的跟控制器,將原本點擊事件放到topVC的touchBegin方法中即可
</ul>