一根时、ScrollToTopButton的由來
前幾天,接到一個(gè)需求:列表從下往上滑動(dòng)超過一屏箕般,出現(xiàn)返回頂部的按鈕,懸停3秒后消失舔清。分析一波丝里,列表其實(shí)就是UITableView,大家都知道當(dāng)UIScrollView的scrollsToTop屬性賦值為true時(shí)鸠踪,點(diǎn)擊狀態(tài)欄丙者,是會(huì)返回到頂部的,那為啥還需要這個(gè)需求呢营密?這個(gè)問題就問的好了械媒,下圖一為產(chǎn)品經(jīng)理的一波解釋,而圖二是同事們的討論评汰。我覺得纷捞,加不加倒也沒什么所謂,不過當(dāng)效果出來后被去,用著確實(shí)還挺不錯(cuò)的主儡。
關(guān)于ScrollToTopButton,其實(shí)它是一個(gè)view惨缆,然后addSubview一個(gè)UIButton糜值,為啥這么寫呢?唔...方便適配坯墨?姑且就這么覺得吧寂汇。那為什么叫ScrollToTopButton?顧名思義捣染,滾到頂部的按鈕嘛骄瓣,沒辦法,實(shí)在想不到好點(diǎn)的命名耍攘。
二榕栏、來一波運(yùn)行效果
啊哈哈哈,挺不錯(cuò)的吧蕾各!接下來扒磁,看下具體代碼和實(shí)現(xiàn)吧...
三、分析一波
關(guān)于如何將ScrollToTopButton添加到view上式曲,有兩種方案:一是寫一個(gè)UIScrollView擴(kuò)展渗磅,在擴(kuò)展中,寫一個(gè)相關(guān)方法,然后在對(duì)應(yīng)界面的controller中始鱼,調(diào)用擴(kuò)展的方法即可仔掸;二則是用runtime,在load函數(shù)中医清,替換系統(tǒng)UIView的didMoveToSuperview方法起暮,在替換的方法中,添加ScrollToTopButton会烙。但都存在一些問題负懦。
1、關(guān)于ScrollToTopButton的實(shí)現(xiàn)
什么柏腻,你要看我寫的垃圾??代碼纸厉?不好吧?在這里五嫂,我就不把我的垃圾代碼貼出來了颗品。在文章最后會(huì)貼出GitHub的地址,有興趣的同學(xué)可以去查看沃缘,有什么建議躯枢,歡迎大神留言指導(dǎo)。
代碼的相關(guān)說明
- 關(guān)于KVO的observe方法槐臀,為啥會(huì)沒有相關(guān)監(jiān)聽方法锄蹂?為啥沒有removeObserver方法,這不會(huì)crash嗎水慨?這篇文章會(huì)給你一點(diǎn)解答得糜。分享一個(gè)關(guān)于KVO的擴(kuò)展,如果不想導(dǎo)入FBKVOController晰洒,以及該KVO擴(kuò)展的話朝抖,只需要將observe的寫法改成系統(tǒng)的就可以了,別忘了要在適當(dāng)?shù)臅r(shí)候removeObserver欢顷,不然會(huì)crash的。
- 關(guān)于需求中的捉蚤,當(dāng)停止?jié)L動(dòng)后抬驴,懸停3秒后隱藏。我這里使用的方法是
open func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)
來延遲隱藏缆巧,以及open class func cancelPreviousPerformRequests(withTarget aTarget: Any, selector aSelector: Selector, object anArgument: Any?)
方法來進(jìn)行取消延遲的執(zhí)行布持。
為什么不用GCD的dispatch_after進(jìn)行延遲呢?根據(jù)我的了解陕悬,dispatch_after一旦延遲后题暖,好像沒有相關(guān)的方法取消延遲。也就是說,當(dāng)我們停止?jié)L動(dòng)后胧卤,會(huì)調(diào)用dispatch_after唯绍,但當(dāng)我們?cè)俅螡L動(dòng)時(shí),dispatch_after延遲是不會(huì)被取消的枝誊,延遲設(shè)置的時(shí)間到后况芒,還是會(huì)被延遲的代碼。
performSelector方式進(jìn)行的延遲叶撒,可以調(diào)用cancelPreviousPerformRequests方法來進(jìn)行取消绝骚。需要注意的是,在調(diào)用該方法時(shí)祠够,需要傳入之前被延遲的方法压汪。參考文章:取消延遲執(zhí)行函數(shù) cancelPreviousPerformRequestsWithTarget
2、如何將ScrollToTopButton添加到view上古瓤?
(1)寫一個(gè)UIScrollView擴(kuò)展
寫擴(kuò)展的好處就是止剖,只要是UIScrollView或者繼承自UIScrollView,就能調(diào)用擴(kuò)展的方法湿滓。為什么要返回ScrollToTopButton滴须,不返回可以嗎?返回ScrollToTopButton叽奥,你可以將其存起來扔水,然后進(jìn)行判斷,如果為nil時(shí)朝氓,才調(diào)用addScrollToTopBtn方法魔市。其實(shí)不這樣做也行,寫在viewDidload方法赵哲,應(yīng)該就沒什么問題待德。
extension UIScrollView {
func addScrollToTopBtn() -> ScrollToTopButton {
return ScrollToTopButton(frame: CGRect(x: (self.width - 40) / 2, y: self.height + 100, width: 40, height: 40), scrollView: self)
}
}
調(diào)用該方法:如果該方法有返回值:_ = tableView.addScrollToTopBtn()
,_是你定義的相關(guān)變量枫夺,這里我就省略不寫了将宪;如果沒有返回值:tableView.addScrollToTopBtn()
。是不是覺得很簡(jiǎn)單方便橡庞,控制器根本不需要關(guān)心ScrollToTopButton是如何實(shí)現(xiàn)以及如何添加到view上较坛。
之前說到,用擴(kuò)展的方法扒最,是會(huì)有問題的丑勤。假如,整個(gè)app的列表有幾十個(gè)吧趣,那你就需要在這幾十個(gè)列表的控制器法竞,一一paste這行代碼tableView.addScrollToTopBtn()
耙厚,程序猿都是“很懶的”,那有沒有辦法可以岔霸,每個(gè)列表的控制器都不用寫這行代碼薛躬,就可以將返回頂部的按鈕,添加到所有列表中呢秉剑?請(qǐng)看第二種方法泛豪。
(2)利用Method Swizzling - 方法交換
如果有同學(xué)不懂Method Swizzling,推薦看下玉令天下的一篇博客侦鹏,Objective-C Method Swizzling诡曙,寫的很詳細(xì)。當(dāng)然也可以通過其他的文章進(jìn)行學(xué)習(xí)了解略水。
這里我們利用runtime的方法交換价卤,通過自己的方法替換系統(tǒng)方法,在自己的方法里面添加判斷渊涝,從而將按鈕添加到列表中慎璧。需求中,是當(dāng)我們滑動(dòng)列表時(shí)跨释,將返回頂部的按鈕顯示胸私,那第一個(gè)就是想到,能否替換scrollViewDidScroll
的方法鳖谈,經(jīng)過一番嘗試之后岁疼,很可惜并不行。runtime的方法交換缆娃,能適用于替換類本身的方法捷绒。scrollView的代理方法不行,第二個(gè)想到的就是替換contentOffset的set方法贯要,但最后的方案是選擇替換UIView的didMoveToSuperview
方法暖侨,這個(gè)方法是當(dāng)view的父級(jí)視圖更改的時(shí)候會(huì)調(diào)用此方法,因此我們就替換這個(gè)系統(tǒng)方法崇渗。
先新建UIScrollView的擴(kuò)展字逗,UIScrollView+Runtime,導(dǎo)入#import <objc/runtime.h>
宅广,在load方法中(如果是Swift的話葫掉,則在initialize方法中),進(jìn)行方法的替換乘碑。為什么要在load方法中挖息,可以通過這篇文章進(jìn)行了解iOS - + initialize 與 +load金拒∈薹簦可能有一些文章套腹,會(huì)說在load方法中,寫一個(gè)dispatch_once资铡,讓代碼只執(zhí)行一次电禀。其實(shí),這是沒必要多加一個(gè)dispatch_once的笤休,因?yàn)楸旧韑oad方法只會(huì)進(jìn)一次而已尖飞。所以加不在dispatch_once,其實(shí)沒什么關(guān)系店雅。load的具體實(shí)現(xiàn)如下:
+ (void)load {
Method ori_Method = class_getInstanceMethod([UIScrollView class], @selector(didMoveToSuperview));
Method ud_Mothod = class_getInstanceMethod([UIScrollView class], @selector(ud_didMoveToSuperview));
method_exchangeImplementations(ori_Method, ud_Mothod);
}
- (void)ud_didMoveToSuperview {
[self ud_didMoveToSuperview];
if (self.superview && ([self isMemberOfClass:[UITableView class]])) {
for (UIView *view in self.superview.subviews) {
if ([view isKindOfClass:[ScrollToTopButton class]]) {
return;
}
}
[[ScrollToTopButton alloc] initWithFrame:CGRectMake(self.width, self.height, 48, 48) scrollView:(UIScrollView *)self];
}
}
當(dāng)代碼寫完之后政基,一個(gè)Command+R,結(jié)果crash了闹啦。
在crash信息可以看出沮明,是因?yàn)?code>didMoveToSuperview方法出的問題。原來窍奋,UIScrollView沒有實(shí)現(xiàn)
didMoveToSuperview
方法荐健,而直接交換 IMP 是很危險(xiǎn)的。因?yàn)槿绻@個(gè)類中沒有實(shí)現(xiàn)這個(gè)方法琳袄,class_getInstanceMethod()
返回的是某個(gè)父類的 Method 對(duì)象江场,這樣method_exchangeImplementations()
就把父類的原始實(shí)現(xiàn)(IMP)跟這個(gè)類的 Swizzle 實(shí)現(xiàn)交換了。這樣其他父類及其其他子類的方法調(diào)用就會(huì)出問題窖逗,最嚴(yán)重的就是 Crash址否。那怎么辦?那就不能用UIScrollView的擴(kuò)展了滑负,但是我們可以改成UIView的擴(kuò)展在张,效果也是一樣的。
修改之后矮慕,再次運(yùn)行帮匾。不會(huì)crash了,隨便找了個(gè)列表滑動(dòng)后痴鳄,返回頂部的按鈕也顯示出來瘟斜,那就說明,用runtime的方法已經(jīng)可行痪寻。但是螺句,因?yàn)槭荱IView的擴(kuò)展,我們?cè)谧约旱?code>ud_didMoveToSuperview橡类,需要對(duì)當(dāng)前的self進(jìn)行判斷蛇尚,
[self isMemberOfClass:[UITableView class]
,是UITableView顾画,我們才添加返回頂部的按鈕取劫。這里需要特別特別注意的:在我們替換的方法中匆笤,一定要調(diào)用自身的方法,非系統(tǒng)的方法谱邪,不然會(huì)導(dǎo)致死循環(huán)的炮捧。
[self ud_didMoveToSuperview];
這行代碼實(shí)際是調(diào)用系統(tǒng)的didMoveToSuperview
方法。
那用Method Swizzling進(jìn)行方法交換的方案有什么問題呢惦银?
- 用這個(gè)方案咆课,是所有列表的都添加了,但如果我有些列表不要添加呢扯俱?是不是覺得有坑了书蚪?有一種方法是,在
ud_didMoveToSuperview
這個(gè)方法中迅栅,對(duì)需要添加的列表進(jìn)行if判斷善炫,可是這樣做又破壞了封裝。 - 第二個(gè)問題是ScrollToTopButton的frame不對(duì)的問題库继。因?yàn)槲覀兪悄?code>scrollView.superview來進(jìn)行計(jì)算的箩艺,如果view的底部還有一個(gè)類似的tool view的呢?那ScrollToTopButton的frame就計(jì)算錯(cuò)誤了宪萄。
- 還有一個(gè)問題就是艺谆,會(huì)發(fā)現(xiàn)這個(gè)ScrollToTopButton,只有創(chuàng)建拜英,沒有remove静汤。隱藏后,也只是hidden居凶,并沒有從當(dāng)前的view中remove虫给,這也需要解決的問題。
-
對(duì)于上面問題侠碧,目前還真沒有想出比較好的解決方案抹估,如果有更好的解決方案的同學(xué),歡迎可以通過留言指導(dǎo)弄兜。
Paste_Image.png
(3)除了上面兩種方案药蜻,那有沒有第三種方案?答案是肯定的替饿。
我們也可以在每個(gè)需要添加的列表的控制器中语泽,都寫一模一樣的代碼,從創(chuàng)建按鈕到添加视卢。
但是這種寫法踱卵,不但惡心了自己,更惡心了別人据过。這種重復(fù)的代碼根本沒有可維護(hù)而言惋砂,如果哪天產(chǎn)品改需求了蔬充,這個(gè)按鈕需要換個(gè)位置,那你就崩潰了班利。少寫一些重復(fù)的代碼,多寫一些已維護(hù)的榨呆,多用擴(kuò)展罗标,封裝。积蜻。闯割。
四、來個(gè)demo
ScrollToTopButtonDemo
如果有興趣的同學(xué)竿拆,可以下載這個(gè)很簡(jiǎn)易的demo宙拉,寫的有點(diǎn)爛,不過重點(diǎn)是上面所說的思路和實(shí)現(xiàn)方法丙笋。我寫的垃圾代碼谢澈,看看就好。