iOS開發(fā) - 列表返回頂部按鈕(ScrollToTopButton)

一根时、ScrollToTopButton的由來

Paste_Image.png

前幾天,接到一個(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)的命名耍攘。


圖一.png
圖二.png

二榕栏、來一波運(yùn)行效果

啊哈哈哈,挺不錯(cuò)的吧蕾各!接下來扒磁,看下具體代碼和實(shí)現(xiàn)吧...


run.gif

三、分析一波

關(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了闹啦。

Paste_Image.png

在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
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)方法丙笋。我寫的垃圾代碼谢澈,看看就好。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末御板,一起剝皮案震驚了整個(gè)濱河市锥忿,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌怠肋,老刑警劉巖敬鬓,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異笙各,居然都是意外死亡钉答,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門杈抢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來数尿,“玉大人,你說我怎么就攤上這事惶楼∑龃矗” “怎么了瘤旨?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵笆环,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我但狭,道長(zhǎng)窥岩,這世上最難降的妖魔是什么甲献? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮颂翼,結(jié)果婚禮上晃洒,老公的妹妹穿的比我還像新娘慨灭。我一直安慰自己,他們只是感情好球及,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布氧骤。 她就那樣靜靜地躺著,像睡著了一般吃引。 火紅的嫁衣襯著肌膚如雪筹陵。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天镊尺,我揣著相機(jī)與錄音朦佩,去河邊找鬼。 笑死庐氮,一個(gè)胖子當(dāng)著我的面吹牛语稠,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播弄砍,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼仙畦,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了音婶?” 一聲冷哼從身側(cè)響起议泵,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎桃熄,沒想到半個(gè)月后先口,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡瞳收,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年碉京,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片螟深。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谐宙,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出界弧,到底是詐尸還是另有隱情凡蜻,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布垢箕,位于F島的核電站划栓,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏条获。R本人自食惡果不足惜忠荞,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧委煤,春花似錦堂油、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至讥邻,卻和暖如春迫靖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背计维。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留撕予,地道東北人鲫惶。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像实抡,于是被迫代替她去往敵國和親欠母。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,146評(píng)論 30 470
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理吆寨,服務(wù)發(fā)現(xiàn)赏淌,斷路器,智...
    卡卡羅2017閱讀 134,659評(píng)論 18 139
  • 新海誠2013年作品 故事:女教師與高中生之間陌生的微妙的感情,他們都是生活在正常人之外的人辣卒,高中生整天打工掷贾、制鞋...
    徐徐圖之Q閱讀 602評(píng)論 0 0
  • 閉上眼想想 多年以后 在遠(yuǎn)山同去回來的路上 你唱歌 樹葉聽著,我聽著荣茫。 11.28
    花花cq閱讀 325評(píng)論 0 2
  • 最近生活太累想帅,工作太忙,于是在朋友圈發(fā)牢騷啡莉,我是個(gè)害怕麻煩的人港准,只寫了“生無可戀”四個(gè)字,然后把手機(jī)丟在一...
    echo兔閱讀 341評(píng)論 0 0