iOS組件封裝的思路和實現

    前兩天面試了一個應聘者,他的演示項目裏有廣告輪播功能。恰好之前我封裝過一個實現了此功能的控件,於是就順着他廣告輪播的實現一直往下聊,從需求的抽象一直聊到各種實現的細節和需要考慮的問題等等。組件的封裝是開發中比較有趣的一件事。今天我們就拿輪播控件舉例,聊聊組件的封裝。授人予漁先要授人予魚。先給出魚(PSCarouselView:https://github.com/DeveloperPans/PSCarouselView),再慢慢講漁。


分析需求

    一般來說,我們在封裝組件的時候,會先思考以下幾點

    • 這個組件要做什麼

    • 這個組件至少需要知道哪些信息

    • 這個組件會反饋哪些信息


    這個階段,我們還不會考慮組件的具體實現,僅僅對其做抽象。我們要做的東西不僅僅是適用於單個項目的,而應該是通用的,可以適應大部分同種類需求的。


Scene I

    BA說,客戶要在首頁加個輪播。於是半天后設計師出了張圖,看起來大概像這樣:




我們按照上面的思路,拆分一下這個需求。


這個組件要做什麼

    顯而易見的:

        1.展示多張圖片

        2.可以向左向右翻頁

        3.PageControl的狀態會根據圖片的滾動而相應改變

    隱含可能要做的:

        1.支持左右兩側無限循環滾動

        2.自動輪播

        3.支持手動滑動

        4.支持點擊並進行相關的響應

        5.圖片的緩存


這個組件至少需要知道哪些信息

    一個封裝得優秀的複雜度不高組件就像一個魔法盒子,只需要觸發啓動開關,就可以達到你期待的效果。極簡的觸發參數和條件是組件封裝的精髓。在內容型App中,輪播圖一般會用作推薦內容展示區域。在O2O類App中,輪播圖一般會用作廣告位。因此輪播區域圖片的內容絕大多數都是動態的。在一般的C/S開發中,客戶端要獲取存儲在服務器上的圖片會獲取它的URL,然後在需要的時候根據URL異步地加載這些圖片。因此,我們找出了的輪播空間第一個必備條件:

    一個圖片URL地址數組。

@property (nonatomic, strong) NSArray *imageURLs;
/**< 必須賦值。只要給這個imageURL賦值,會自動獲取圖片*/

    這個時候我們已經可以根據數組內的URL,數組內URL的數量,完成輪播效果了。但還不夠完善。試想,當數組爲空的時候,我們的輪播控件是一個什麼狀態?網絡比較慢,圖片還在加載中的時候,我們的輪播控件是什麼狀態?就目前來說----空白一片。

每個App都有每個App的設計,但無論如何誰也不會容忍首頁最醒目的部分出現一大片空白,因此佔位圖是必須的。我們告訴我們的控件,“沒有圖片的時候別傻愣着,顯示這張圖”。必備條件二:


    佔位圖

@property (nonatomic, strong) UIImage *placeholder;/**< 沒有輪播圖時的佔位圖*/

    有了以上信息我們的輪播控件已經可以在運行得比較好了。但是缺少了一定的定製性,能滿足的需求面還不夠廣。比如說,讓控件在需要自動輪播的地方自動輪播,在不需要自動輪播的地方保留無限滾動的特性,可以手動撥動?(之前有個產品提過這樣的需求)比如說,這個控件是否應該提供可選的PageControl實現?依據設計師給的圖不同,我們可能需要實現不同的PageControl,不管是顏色,佈局,還是其可見隱藏都可能會有不同。這一部分複雜度不應該歸到輪播控件中。但考慮到一些快速開發的需求,控件還是提供了一個默認選項。這些都是讓控件變得更加可配置和靈活(同時也變得複雜)的可選信息。綜上我們大概可以整理出以下屬性:

@property (nonatomic,getter=isAutoMoving) BOOL autoMoving;/**< 是否自動輪播,默認爲NO*/

@property (nonatomic,getter=isShowPageControl) BOOL showPageControl;/**< 是否展示默認的PageControl,默認爲YES*/

這個組件會反饋哪些信息

    上一點中,如果使用者需要自己實現PageControl,那麼當前控件滾動到哪一頁了,應該給出反饋。

    不論在O2O的App中,還是資訊類App中,用戶點擊輪播必定會有相對應的響應,例如之乎日報點擊跳轉到對應的文章,淘寶點擊查看大圖等。因此用戶點擊的信息也必須反饋出來,以讓控制器根據用戶的操作進行不同的響應。

View層對Controller層的反饋一般有兩種,一種是Target-Action,一種是Delegate。此處我們類似UITableView,給出一定的代理方法。遵循蘋果的設計規範,讓使用的開發者容易上手。

@protocol PSCarouselDelegate <NSObject>

@optional
/** *  告訴代理滾動到哪一頁了 * *  @param carousel self
*  @param page  已經計算好,直接使用即可
*/ - (void)carousel:(PSCarouselView *)carousel didMoveToPage:(NSUInteger)page; /** *  告訴代理用戶點擊了某一頁 * *  @param carousel
*  @param index  imageURL的index  
*/ - (void)carousel:(PSCarouselView *)carousel didTouchPage:(NSUInteger)page;

@end

    至此,輪播組件的架子已經明晰,剩下的就是如何實現,以及實現的時候需要考慮的一些細節。


實現過程

    選擇基類


    蘋果的UIKit提供了非常多優秀的類,可以作爲我們輪播視圖的基類。根據以上的分析,我們根據直覺,立馬可以聯想到以下三個類:UIPageViewControllerUIScrollViewUICollectionView

UIPageViewController提供了非常好的翻頁封裝,僅需要指定翻頁的方向,和子ViewController的數組,就可以提供類似輪播的功能。並且UIPageViewController提供了供翻頁的接口和必要的反饋信息,定義在UIPageViewControllerDelegate協議中。用UIPageViewController,我們只需要將每個輪播頁封裝成一個僅包含一個UIImageView的ViewController,並將其設爲UIPageViewController的ViewControllers,並實現它的代理方法就很容易實現我們的輪播圖。


    UIScrollView提供了最基本的滾動封裝。採用UIScrollView作爲基類,需要自己根據圖片的數量計算ContentSize,並在ScrollView相對應的代理方法中,根據Frame相關信息來計算頁碼,會比UIPageViewController稍微複雜。但好在,它是一個View


    UICollectionView做爲UIScrollView的子類提供了比UIScrollView更好的封裝,也提供了"翻頁"的接口,並提供了一系列定位CollectionView的狀態信息。相比UIScrollView,他提供了更深層的封裝。同UIPageViewController一樣,非常適合本作爲本組件的基類。同時,當UICollectionView的一些代理方法不足以提供相關信息時,還可以通過UIScrollView的代理方法來解決。


    採用UIPageViewController作爲子類,考慮到使用者需要將其作爲subViewController添加到它們的項目中,沒有將一個View添加到ViewController中來得直觀,綜合考慮,PSCarouselView選擇了UICollectionView作爲其基類。


實現功能

    如何利用UICollectionView在有限的圖片數量下實現無限輪播?我們只需要N+2個Cell就可以實現無限輪播。如圖


    將imageURL數組中的URL,擴充爲N+2。同時,將imageURLs的最後一個URL作爲新數組的第一個URL,將imageURLs的第一個URL作爲新數組的最後一個URL。然後在我們的CollectionView滾動到最後一個Cell時,跳轉到第二個URL表示的IndexPath即可。自動輪播採用計時器來完成。在實現的時候,我們需要注意一下的幾點


    1.計時器的開啓與暫停

        自動輪播,通常我們會使用到計時器NSTimer。當我們在頁面切換的過程中,需要注意計時器的開啓與暫停,不然可能會出現一些不可預料的BUG。而一個ViewController的生命週期,View是不可能知道的。因此我們需要提供兩個接口,供使用者開啓/暫停計時器。

- (void)startMoving;- (void)stopMoving;

    另外,我們還需要注意App的生命週期,當App進入不活動的狀態時,我們也需要將計時器暫停,並在回到活動狀態時相應地啓動。

#pragma mark - Notification
//程序被暫停的時候,應該停止計時器
- (void)applicationWillResignActive{  
 [self stopMoving]; }
//程序從暫停狀態迴歸的時候,重新啓動計時器
- (void)applicationDidBecomeActive{    
 if (self.isAutoMoving) {
       [self startMoving];    } }

    2.用戶操作與自動輪播的衝突 
        用戶操作時如果輪播還是繼續滾動,會導致用戶產生不可控的錯覺。《iOS Human Interface Guide》中強調過App需要給於用戶控制感。因此,在用戶手動"翻頁"的時候,我們需要暫時暫停一下自動輪播,並在用戶手動"翻頁"完成後,重新進行輪播。

//用戶手動拖拽,暫停一下自動輪播
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView{  
    [self removeTimer]; }
//用戶拖拽完成,恢復自動輪播(如果需要的話)
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {    
   if (self.isAutoMoving) {
       [self addTimer];    } }

    3.圖片的緩存與展示 
        如何通過圖片的URL來獲得相對應的圖片,並做好圖片的緩存,以實現最大限度地節省流量?這個模塊其實應該單獨做一個有關圖片緩存的庫,而不應該包含到View層中來。本作採用了SDWebImage來實現這一功能。有興趣的朋友可以嘗試着實現一個圖片緩存庫。


    以上,一個輪播組件的封裝基本上完成了。 That's all.Hope you enjoy it :)


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章