寫在前面
做過移動端開發的人都知道,清單控制項是最常用的控制項之一。iOS裡的清單控制項是UITableView,其實Apple的開發人員對於UITableView的設計已經夠好的了(簡單易用,擴充性非常強等等)。
但對於展示邏輯單一的移動端系統軟體,你還是能感覺到有些繁瑣(或許是程式員天生就有些懶惰的毛病吧)。
來看看它到底繁瑣在哪兒了。首先,它的使用頻率太高了;第二,它通常不是只呈現一下資料就完事了,一般都會跟隨下拉重新整理、上提載入更多功能,當然通常還要跟網路下載資料、圖片打交道;第三,MVC模式是ios開發的慣用模式,隨之而來的是一大堆協議的實現(無論你是再寫一次也好,拷貝也罷,反正做這些工作都讓人覺得索然無味)。
衝著這些,今天就把UITableView常見的使用模式封裝了一下。具體做了以下幾件事:
1、 內嵌了下拉重新整理(EGORefreshTableHeaderView)、上提載入更多(LoadMoreTableFooterView)
2、 內建實現了UITableViewDataSource、UITableViewDelegate這兩個通常必須實現的協議,對於自實現的邏輯以Block的形式對客戶代碼開放
3、 內建實現了1中提到的兩個組件的回調協議,同上,自實現的邏輯以Block的形式對外開放
4、 內建實現了EGORefreshTableHeaderView、LoadMoreTableFooterView與UITableView互動必須實現的UIScrollViewDelegate協議
5、 內建實現了非同步圖片下載(可選)
你可以到我的Github上,查看源碼。稱它為ELTableViewController是取了EGORefreshTableHeaderView以及LoadMoreTableFooterView的首字母。
這份代碼中包含了一個樣本程式以及三個必備組件:
1、 EGORefreshTableHeaderView
2、 LoadMoreTableFooterView(修改版,原版不能適應任何尺寸的高度)
3、 Apple官方提供的非同步下載UITableView中的圖片的樣本組件(IconDownLoader),這個只適用於下載類似於社交網路中的帳戶圖片,不建議使用它來下載那些大圖片,因為它甚至都沒有緩衝(如果圖片很大,推薦使用SDImage)
代碼解讀
它已經內建實現了這些協議,所以在你使用它的時候,無需設定和實現。
@interface ELTableViewController : UIViewController<UITableViewDelegate,UITableViewDataSource,EGORefreshTableHeaderDelegate,LoadMoreTableFooterDelegate,IconDownloaderDelegate>
對於不斷變化的商務邏輯,這裡提供了所有需要實現的block:
//blocks for UITableView delegatetypedef UITableViewCell* (^cellForRowAtIndexPathDelegate) (UITableView *,NSIndexPath *);typedef CGFloat (^heightForRowAtIndexPathDelegate) (UITableView *,NSIndexPath *);typedef void (^didSelectRowAtIndexPathDelegate) (UITableView *,NSIndexPath *);//blocks for refresh and load moretypedef void (^refreshDataSourceFunc) (void);typedef void (^loadMoreDataSourceFunc) (void);typedef void (^refreshDataSourceCompleted) (void);typedef void (^loadMoreDataSourceCompleted) (void);//use to load image (async)typedef void (^loadImagesForVisiableRowsFunc) (void);typedef void (^appImageDownloadCompleted) (NSIndexPath *);
它們以屬性的形式對外公開:
//property for blocks@property (nonatomic,copy) cellForRowAtIndexPathDelegate cellForRowAtIndexPathDelegate;@property (nonatomic,copy) heightForRowAtIndexPathDelegate heightForRowAtIndexPathDelegate;@property (nonatomic,copy) didSelectRowAtIndexPathDelegate didSelectRowAtIndexPathDelegate;@property (nonatomic,copy) loadMoreDataSourceFunc loadMoreDataSourceFunc;@property (nonatomic,copy) refreshDataSourceFunc refreshDataSourceFunc;@property (nonatomic,copy) refreshDataSourceCompleted refreshDataSourceCompleted;@property (nonatomic,copy) loadMoreDataSourceCompleted loadMoreDataSourceCompleted;@property (nonatomic,copy) loadImagesForVisiableRowsFunc loadImagesForVisiableRowsFunc;@property (nonatomic,copy) appImageDownloadCompleted appImageDownloadCompleted;
對於上提載入更多、下拉重新整理、圖片非同步載入這幾個功能都是可選的,它們以組件的形式存在。比如,在執行個體化該controller的時候你就可以設定上提和下拉是否可用。而對於圖片下載,你只要不實現其相應得block,它也不會對你造成額外的負擔。
- (id)initWithRefreshHeaderViewEnabled:(BOOL)enableRefreshHeaderView andLoadMoreFooterViewEnabled:(BOOL)enableLoadMoreFooterView;- (id)initWithRefreshHeaderViewEnabled:(BOOL)enableRefreshHeaderView andLoadMoreFooterViewEnabled:(BOOL)enableLoadMoreFooterView andTableViewFrame:(CGRect)frame;
#pragma mark - UITableView Delegate -- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{ if (nil==self.dataSource) { return 0; } return [self.dataSource count];}- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ if (!self.cellForRowAtIndexPathDelegate) { @throw [NSException exceptionWithName:@"Framework Error" reason:@"Must be setting cellForRowAtIndexPathBlock for UITableView" userInfo:nil]; } return self.cellForRowAtIndexPathDelegate(tableView,indexPath);}- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{ if (!self.heightForRowAtIndexPathDelegate) { @throw [NSException exceptionWithName:@"Framework Error" reason:@"Must be setting heightForRowAtIndexPathDelegate for UITableView" userInfo:nil]; } return self.heightForRowAtIndexPathDelegate(tableView,indexPath);}- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath{ if (self.didSelectRowAtIndexPathDelegate) { self.didSelectRowAtIndexPathDelegate(tableView,indexPath); }}#pragma mark - LoadMoreTableFooterDelegate Methods -- (void)loadMoreTableFooterDidTriggerRefresh:(LoadMoreTableFooterView *)view{ if (self.loadMoreDataSourceFunc&&self.loadMoreDataSourceCompleted) { self.loadMoreDataSourceFunc(); double delayInSeconds = 3.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); dispatch_after(popTime, dispatch_get_main_queue(), self.loadMoreDataSourceCompleted); }}- (BOOL)loadMoreTableFooterDataSourceIsLoading:(LoadMoreTableFooterView *)view{ return self.isLoadingMore;}#pragma mark - EGORefreshTableHeaderDelegate Methods --(void)egoRefreshTableHeaderDidTriggerRefresh:(EGORefreshTableHeaderView *)view{ if (self.refreshDataSourceFunc&&self.refreshDataSourceCompleted){ self.refreshDataSourceFunc(); double delayInSeconds = 3.0; dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC); dispatch_after(popTime, dispatch_get_main_queue(), self.refreshDataSourceCompleted); }}-(BOOL)egoRefreshTableHeaderDataSourceIsLoading:(EGORefreshTableHeaderView *)view{ return self.isRefreshing;}-(NSDate *)egoRefreshTableHeaderDataSourceLastUpdated:(EGORefreshTableHeaderView *)view{ return [NSDate date];}#pragma mark - UIScrollViewDelegate Methods --(void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView{ self.currentOffsetPoint=scrollView.contentOffset;}-(void)scrollViewDidScroll:(UIScrollView *)scrollView{ CGPoint pt=scrollView.contentOffset; if (self.currentOffsetPoint.y<pt.y) { [self.loadMoreFooterView loadMoreScrollViewDidScroll:scrollView]; }else { [self.refreshHeaderView egoRefreshScrollViewDidScroll:scrollView]; }}-(void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{ CGPoint pt=scrollView.contentOffset; if (self.currentOffsetPoint.y<pt.y) { [self.loadMoreFooterView loadMoreScrollViewDidEndDragging:scrollView]; }else { [self.refreshHeaderView egoRefreshScrollViewDidEndDragging:scrollView]; } if (!decelerate&&self.loadImagesForVisiableRowsFunc) { self.loadImagesForVisiableRowsFunc(); }}-(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{ if (self.loadImagesForVisiableRowsFunc) { self.loadImagesForVisiableRowsFunc(); }}#pragma mark - download image async --(void)appImageDidLoad:(NSIndexPath *)indexPath{ if (self.appImageDownloadCompleted) { self.appImageDownloadCompleted(indexPath); }}
ELTableViewController 的使用
建立一個新的controller繼承自:ELTableViewController;
override父類的initBlocks方法:
#pragma mark - private methods -- (void)loadDataSource{ self.dataSource=[NSMutableArray array]; [self.dataSource addObject:@"dataSource_1"]; [self.dataSource addObject:@"dataSource_2"]; [self.dataSource addObject:@"dataSource_3"]; [self.dataSource addObject:@"dataSource_4"]; [self.dataSource addObject:@"dataSource_5"]; [self.dataSource addObject:@"dataSource_6"]; [self.dataSource addObject:@"dataSource_7"]; [self.dataSource addObject:@"dataSource_8"]; [self.dataSource addObject:@"dataSource_9"]; [self.dataSource addObject:@"dataSource_10"];}- (void)initBlocks{ __block TestViewController *blockedSelf=self; //load more self.loadMoreDataSourceFunc=^{ [blockedSelf.dataSource addObject:@"loadMoreDataSourceBlock_1"]; [blockedSelf.dataSource addObject:@"loadMoreDataSourceBlock_2"]; [blockedSelf.dataSource addObject:@"loadMoreDataSourceBlock_3"]; [blockedSelf.dataSource addObject:@"loadMoreDataSourceBlock_4"]; [blockedSelf.dataSource addObject:@"loadMoreDataSourceBlock_5"]; blockedSelf.isLoadingMore=YES; [self.tableView reloadData]; NSLog(@"loadMoreDataSourceBlock was invoked"); }; //load more completed self.loadMoreDataSourceCompleted=^{ blockedSelf.isLoadingMore=NO; [blockedSelf.loadMoreFooterView loadMoreScrollViewDataSourceDidFinishedLoading:self.tableView]; NSLog(@"after loadMore completed"); }; //refresh self.refreshDataSourceFunc=^{ blockedSelf.dataSource=[NSMutableArray array]; [blockedSelf.dataSource addObject:@"refreshDataSourceBlock_1"]; [blockedSelf.dataSource addObject:@"refreshDataSourceBlock_2"]; [blockedSelf.dataSource addObject:@"refreshDataSourceBlock_3"]; [blockedSelf.dataSource addObject:@"refreshDataSourceBlock_4"]; [blockedSelf.dataSource addObject:@"refreshDataSourceBlock_5"]; blockedSelf.isRefreshing=YES; [self.tableView reloadData]; NSLog(@"refreshDataSourceBlock was invoked"); }; //refresh completed self.refreshDataSourceCompleted=^{ blockedSelf.isRefreshing=NO; [blockedSelf.loadMoreFooterView loadMoreScrollViewDataSourceDidFinishedLoading:self.tableView]; NSLog(@"after refresh completed"); }; self.cellForRowAtIndexPathDelegate=^(UITableView *tableView, NSIndexPath *indexPath){ static NSString *cellIdentifier=@"cellIdentifier"; UITableViewCell *cell=[tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (!cell) { cell=[[[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier]autorelease]; } cell.textLabel.text=[blockedSelf.dataSource objectAtIndex:indexPath.row]; NSLog(@"block:cellForRowAtIndexPathBlock has been invoked."); return cell; }; self.heightForRowAtIndexPathDelegate=^(UITableView *tableView, NSIndexPath *indexPath){ NSLog(@"block:heightForRowAtIndexPathBlock has been invoked."); return 60.0f; }; self.didSelectRowAtIndexPathDelegate=^(UITableView *tableView, NSIndexPath *indexPath){ NSLog(@"block:didSelectRowAtIndexPathDelegate has been invoked."); }; }
然後在ViewDidLoad中調用:
[self initBlocks]; [self loadDataSource]; [self.tableView reloadData];
最後,你在執行個體化該controller的時候,可以指定是否使用上提和下拉
self.viewController = [[[TestViewController alloc] initWithRefreshHeaderViewEnabled:YES andLoadMoreFooterViewEnabled:YES]autorelease];
寫在最後
寫完之後,我用它重構了一下快易博中,新浪微博的幾個視圖。也省掉了一些冗餘代碼,如果當初在開發的時候就使用它的話,感覺還是省了一些功夫的。
它其實也還是比較簡單的封裝,所以還不是很具有業務相關性,同時也可見它還有很多可繼續增強功能:
1、 封裝增刪改查功能
2、 封裝載入、操作時動畫
3、 封裝網路載入的統一實現
……………….
先寫到這裡吧。
推薦兩篇講ios block非常不錯的文章:
http://lldong.github.com/blog/2011/12/30/blocks/
http://yannickloriot.com/2011/11/working-with-blocks/
源碼地址:
https://github.com/yanghua/ELTableViewController