UITableView用得較多,遇到的情況也較多,單獨記錄一篇。
一、零散的技巧
二、取cell
三、cell高度
四、導航欄、TableView常見問題相關
五、自定義左滑刪除按鈕圖片
六、僅做了解
一、零散的技巧
1、 cell的選中效果是cell的屬性,可以有的有,無的無。
// 自定義cell self.selectionStyle = UITableViewCellSelectionStyleNone; // 取cell cell.selectionStyle = UITableViewCellSelectionStyleNone;
2、cell的下划線是Table的屬性,全部有,或全部無。
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
3、cell下划線左邊頂住屏幕左邊。
cell.preservesSuperviewLayoutMargins = NO; cell.layoutMargins = UIEdgeInsetsZero; cell.separatorInset = UIEdgeInsetsZero;
后續補充:也可以隱藏掉系統的下划線,自定義LineView,要多寬就多寬,且可以實現不同cell不同下划線樣式。
4、cell的重用ID,可以用類名
NSStringFromClass([MyCell class])
5、根據 indexPath 獲取 cell
[self.mTableView cellForRowAtIndexPath:indexPath];
6、根據 cell 獲取 indexPath
[self.mTableView indexPathForCell:cell];
7、superView
// 第一個superview 是contentView,第二個就cell UITableViewCell *cell = (UITableViewCell*)button.superview.superview;
8、UIScrollView 和 UITableView 的 內邊距差別
// 自動偏移 contentOffset = CGPointMake(0, -200); tableView.contentInset = UIEdgeInsetsMake(200, 0, 200, 0); // 需要設置偏移量。否則停留在偏移量(0.0)。需要再下拉一下, scrollView.contentInset = UIEdgeInsetsMake(200, 0, 200, 0); scrollView.contentOffset = CGPointMake(0, -200);
9、監聽 contentOffset ,可以得到類似拖動代理的效果。如寫第三方給別人用。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ }
10、取消cell的左滑 編輯、刪除 狀態。如,按了其他位置的按鈕,cell不會自動復原。
[self.mTableView setEditing:NO animated:YES];
11、style ,風格樣式
// 分組 風格 // 1、自動間隔開每組 // 2、如需設置間隔需要注意,組頭組尾都要處理。(坑過一次,只設置組尾高度,結果發現怎么還很高,而且不顯示不能為0,要 = CGFLOAT_MIN) // 3、滑動,組頭不會懸停 self.mTableView = [[UITableView alloc]initWithFrame:CGRectZero style:UITableViewStyleGrouped]; // 扁平化 風格 // 1、每組的間隙可通過組頭、組尾,自行調整。(相對上面風格,組頭組尾高度默認為0) // 2、滑動,組頭組尾會懸停 self.mTableView = [[UITableView alloc]initWithFrame:CGRectZero style:UITableViewStylePlain];
12、獲取當前顯示的cells
// 直接得到 cells self.mTableView.visibleCells // 得到 indexPath ,看需求通過 cellForRowAtIndexPath: 轉換。 self.mTableView.indexPathsForVisibleRows
13、滑動時,使用低分辨率圖片,停止時再加載高分辨率圖片。(利用 代理 和上面 “12、獲取當前顯示的cells” )
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate; - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView;
14、UIScrollView、UITableView 實時 位置 相關
參照 《iOS:手勢與矩形、點運算相關》 -> “1、矩形、點運算” -> “4、UIScrollView、UITableView 實時 位置 相關”
15、拖動狀態。比如判斷當前滾動是否拖動引起。拖動的代理只有開始和結束,拖動中沒有。
mScrollView.dragging
16、滾動到具體的cell位置。如,1、外賣,左右tableView聯動。2、聊天,滾動到最新信息。
if (self.dataSource.count > 0) { [self.mTableView scrollToRowAtIndexPath:[NSIndexPath indexPathForRow:self.dataSource.count-1 inSection:0] atScrollPosition:UITableViewScrollPositionBottom animated:YES]; }
17、選中效果,動畫
// 用途可參考外賣、電商類APP(左邊tbV的分類,隨着右邊商品的拖動,跟新cell選中位置) [self.leftTableView selectRowAtIndexPath:[NSIndexPath indexPathForRow:section inSection:0] animated:YES scrollPosition:UITableViewScrollPositionTop];
N、如果一個 tableView 對應多個 dataSource 。通過按鈕切換,那么要考慮,點擊/滑動 切換時,請求返回的數據,是否是當前 “功能選中”的位置,比如:
判斷對比請求前后的字段 parameterDics、狀態。若不是,
1)、可丟棄。
2)、可刷新該狀態對應的 dataSource 數組(有的話),下次切換,可先刷出數據,再請求。界面友好(防止網絡請求,一片空)。
邊輸入邊搜索,同理,避免,如快速刪除完后,又刷出刪除前的請求數據。
二、取cell
1、cell初始化的一些區別
1)、TableViewCell
1-1)、沒注冊
沒注冊的(一開始會取不到): cell = 從隊列取 if(cell取不到) { 創建cell 創建子視圖,加tag } cell從tag取子視圖,刷新 tag 或 屬性
1-2)、注冊
注冊的(100%取得到): cell = 從隊列取(有indexPath的方法) 刷新 tag 或 屬性 ( 系統取不到,會走自定義的initWithStyle:reuseIdentifier: if(cell創建成功) { 創建子視圖,加tag } )
2)、CollectionViewCell
2-1)、沒注冊
2-2)、注冊
注冊的(100%取得到): cell = 從隊列取(有indexPath的方法) if(cell取得到) { (判斷是否有子視圖)創建子視圖 } 刷新 tag 或 屬性 collectionViewCell 流程有點不同 1、沒 TableViewCell 的 initWithStyle:reuseIdentifier: 2、但 每次都能從隊列取到 3、所以 需要判斷取到的cell是否有子視圖,不然會不斷創建
2、加載XIB
1)、從多個cell樣式的XIB加載。只有1個cell樣式,可直接lastObject加載。(先根據不同的ID取,取不到再加載。)
1-1)、獲取XIB里的所有對象
NSArray *cellArry = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([MyTableCell class]) owner:self options:nil];
1-2)、讀取對應的Cell樣式,此時的參數type為枚舉,或基本數據類型。
cell = [cellArry objectAtIndex:type];
2)、在 UIView + xxx 的類別文件里,可以添加這個類。方便加載單種Cell樣式的XIB。
+ (instancetype)viewFromXib { return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self) owner:nil options:nil] lastObject]; }
三、cell高度
0、不固定內容的cell,可弄數組、模型存高度,以免每次計算。
貌似系統計算的(下面的4、5、),耗時都長?復雜的cell滑動不流暢?所以還是能手動就手動咯(下面的2、3、)?
還有,如果是富文本,記得要把font加進去計算,經常算行距的時候,忘了字體大小。
1、全部固定高度
self.tableView.rowHeight = 44;
2、自定義cell類方法
+ (CGFloat)getCellHeight { return 44; } + (CGFloat)getCellHeightWithData:(id)data { // 手動計算label的高度 return 計算高度; }
后續補充:對於固定高度,沒問題,好用。對於根據Data計算的,根據情況保存計算高度。
3、模型(只有屬性的特殊類)
后續補充:通過get方法讀取。需要才計算(懶加載),可能還要判斷是否計算過,否則每次都要計算?
4、系統自動計算(iOS6后,使用 UIView 的 類別 UIConstraintBasedLayoutFittingSize 的方法,控件需要全是 Autolayout 約束?)
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { // 取出不帶 indexPath 的 UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:NSStringFromClass([MyCell class])]; // 填充數據 //cell.model = model[indexPath.row]; [cell initData:data[indexPath.row]]; // 計算高度 // UILayoutFittingCompressedSize 返回最小可能的值 // UILayoutFittingExpandedSize 返回最大可能的值 cellHeight = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height + 0.5f; return cellHeight; }
后續補充:1、根據情況保存計算高度。
2、普通View非Cell,的高度計算也可用,但同樣要 Autolayout 約束。
3、注冊cell,一般是取出帶indexPath的。不帶indexPath一般是在自寫cell重用機制的用的。
但是,注冊cell 還可以取出普通的cell樣式,不帶 indexPath。來填充數據,計算高度。
4、對3、補充,如果是xib可以用 NSBundle。
5、對3、再補充,可以弄個局部變量,用懶加載獲取普通cell,不用每次都獲取。
6、label類,多行,除了 label.numberOfLines = 0。
好像還需要設置 label.preferredMaxLayoutWidth = SCREEN_WIDTH - 20 ;
5、系統自動計算(iOS8后,UITableViewAutomaticDimension,控件需要全是 Autolayout 約束?)
1)、先給cell高度一個估算值,好讓TableView,知道contentSize有多大
tableView.estimatedRowHeight = 80.0f;
2)、設置為自動計算
tableView.rowHeight = UITableViewAutomaticDimension;
iOS8后,UITableViewAutomaticDimension自動計算,不用實現 heightForRowAtIndexPath 了,不過為了兼容ios8前,可能需要再寫、判斷
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { if ( [[[UIDevice currentDevice] systemVersion ] integerValue] >= 8) { return UITableViewAutomaticDimension; } else { } }
四、導航欄、TableView常見問題相關
1、導航欄、TableView
//調整contentInset。
//NO:不調整,按設定的frame、contentInset的顯示
//YES:會調整contentInset.top的高,讓顯示的頂在導航欄下面,【有滑過半透明效果】
self.automaticallyAdjustsScrollViewInsets =NO;
//調整frame
// UIRectEdgeNone //會頂在導航欄下面【沒有滑過半透明效果】
// UIRectEdgeTop //對齊原點
// UIRectEdgeLeft //對齊左邊
// UIRectEdgeBottom //對齊頂部
// UIRectEdgeRight //對齊右邊
// UIRectEdgeAll //對齊所有
self.edgesForExtendedLayout = UIRectEdgeNone;
//導航欄半透明
self.navigationController.navigationBar.translucent = YES;
//隱藏navigationBar(1、它推過的所有的VC共用1個Bar;2、用繼承View的hidden屬性,隱藏不了!)
self.navigationController.navigationBarHidden=YES;
2、iOS11
此處參考自簡書 “iOS 11 安全區域適配總結 ” -- sonialiu
1)、TableView 默認開啟Cell高度估算,關掉。
[UITableView appearance].estimatedRowHeight = 0; [UITableView appearance].estimatedSectionHeaderHeight = 0; [UITableView appearance].estimatedSectionFooterHeight = 0;
2)、ScrollView新增安全區域。
2-1)、如果之前讓TabelView頂住屏幕,然后設置頂部內邊距 = 20+44,剛好在導航欄下面的話,
會被系統向下偏移64的 SafeAreaInsets,再加上自己設置的64,就出現下移64問題。
2-2)、同理,沒導航欄的時候,也會下移20 -> 狀態欄的高度。
2-3)、以前若設置 automaticallyAdjustsScrollViewInsets = YES 讓系統自動調整,不會有問題
解決方案:添加下面,相當於 automaticallyAdjustsScrollViewInsets = NO
#ifdef __IPHONE_11_0 if ([tableView respondsToSelector:@selector(setContentInsetAdjustmentBehavior:)]) { [UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } #endif
2-4)、contentInsetAdjustmentBehavior 其他類型
UIScrollViewContentInsetAdjustmentScrollableAxes: adjustedContentInset = ( 可滾動方向 ? safeAreaInset + contentInset : contentInset );
UIScrollViewContentInsetAdjustmentNever: adjustedContentInset = contentInset;
UIScrollViewContentInsetAdjustmentAlways: adjustedContentInset = safeAreaInset + contentInset;
UIScrollViewContentInsetAdjustmentAutomatic: (controller里automaticallyAdjustsScrollViewInsets = YES) && (controller被navigation包含) == Always,否則 == Axes
五、自定義左滑刪除按鈕圖片
參考自簡書 《【支持iOS11】UITableView左滑刪除自定義 - 實現多選項並使用自定義圖片 》 -- pika11
0、寫在前面
盡管iOS11已經支持自定義刪除圖片了,但還是要兼容以前的。
1、進入編輯模式,標記view為需要layout。
- (void)tableView:(UITableView *)tableView willBeginEditingRowAtIndexPath:(NSIndexPath *)indexPath { self.editingIndexPath = indexPath; [vc.view setNeedsLayout]; } - (void)tableView:(UITableView *)tableView didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath { self.editingIndexPath = nil; }
2、在VC的,layout子View完成的時候,判斷是否需要改變cell刪除樣式
-(void)viewDidLayoutSubviews { [super viewDidLayoutSubviews]; if (self.dataSource.editingIndexPath != nil) { // 改變cell 刪除文字 為 刪除圖片 [self resetCellDeleteButton]; } }
3、配置,不同系統
- (void)resetCellDeleteButton { // 獲取選項按鈕的reference if ( [[[UIDevice currentDevice] systemVersion] integerValue] >= 11 ) { for (UIView *subview in self.mTableView.subviews) { //iOS11(Xcode 8編譯): UITableView -> UITableViewWrapperView -> UISwipeActionPullView if ([subview isKindOfClass:NSClassFromString(@"UITableViewWrapperView")]) { for (UIView *subsubview in subview.subviews) { if ([subsubview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subsubview.subviews count] >= 1) { #warning - 可能需要判斷類型再去改,會比較好,暫時沒去試。 UIButton *deleteButton = subsubview.subviews.lastObject; [self configDeleteButton:deleteButton]; } } } //iOS11(Xcode 9編譯): UITableView -> UISwipeActionPullView else if ([subview isKindOfClass:NSClassFromString(@"UISwipeActionPullView")] && [subview.subviews count] >= 1) { #warning - 可能需要判斷類型再去改,會比較好,暫時沒去試。 UIButton *deleteButton = subview.subviews.lastObject; [self configDeleteButton:deleteButton]; } } } else { // iOS8-10: UITableView -> UITableViewCell -> UITableViewCellDeleteConfirmationView SignCell *tableCell = [self.mTableView cellForRowAtIndexPath:self.dataSource.editingIndexPath]; for (UIView *subview in tableCell.subviews) { if ( [subview isKindOfClass:NSClassFromString(@"UITableViewCellDeleteConfirmationView")] && [subview.subviews count] >= 1) { UIButton *deleteButton = subview.subviews.lastObject; [self configDeleteButton:deleteButton]; } } } }
4、實現刪除樣式
- (void)configDeleteButton:(UIButton*)deleteButton { deleteButton.backgroundColor = kBgColor; [deleteButton setTitle:@"" forState:UIControlStateNormal]; [deleteButton setImage:[UIImage imageNamed:@"delete"] forState:UIControlStateNormal]; }
六、僅做了解
1、cell 異步加載網絡圖片,主線程更新UI。
1)、現在有了 SDWebImage ,只做為一種思路了解
2)、重用機制。
在取 cell 的同時刷新 imageView 的 tag ,當 imageView 異步獲取到圖片,判斷自己的 tag 還是不是請求前傳進來的 index + basetag。
如果不加這樣的判斷,當網絡差,會出現圖片錯位的情況。
3)、cacheDic。
目前寫法,只是一個可變字典。
往后考慮,1)、獲取圖片前,先判斷自定義緩存NSCache(或者字典)是否有相應url名的圖片。
1)、有 -> 直接調用
2)、沒有 -> 去本地查找,url名的圖片
1)、有,提取到緩存NSCache。key = url,object = image。調用
2)、沒有?請求,以url命名保存到本地、緩存,調用。
2)、NSCache設置一定大小,會自動刪除舊。如緩存讀不到,又會去本地讀取,並刷新到NSCache里。
緩存缺陷,如果后台更新圖片,且名字用原來的,就不會被刷新。
0、宏定義 #define kImageBaseTag 2000 1、判斷數據 // 更新imageView的標簽 imgView.tag = indexPath.row + kImageBaseTag; // 在單元格顯示的時候,先清掉 imgView.image = nil; // 判斷是否加載過,有就用,沒有就請求 if ([[self.imageCacheDic allKeys] containsObject:self.dataSource[indexPath.row]]) { imgView.image = [self.imageCacheDic objectForKey:indexPath]; }else{ dispatch_async(_queue, ^{ NSURL *url = [NSURL URLWithString:self.dataSource[indexPath.row]]; [imgView requestImgFromUrl:url cache:self.imageCacheDic index:indexPath]; }); } 2、請求數據 #import "UIImageView+MyWebCache.h" -(void)requestImgFromUrl:(NSURL*)url cache:(NSMutableDictionary*)cache indexPath:(NSIndexPath*)indexPath { NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setURL:url]; [request setHTTPMethod:@"GET"]; [request setTimeoutInterval:3]; [[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { UIImage *image = [UIImage imageWithData:data]; dispatch_async(dispatch_get_main_queue(), ^{ if (image != nil) { // 添加到緩存 [cache setObject:image forKey:indexpath]; // 判斷是否需要刷新 if (self.tag == indexPath.row + kBaseImageTag) { self.image = image; } } }); }] resume]; }
2、自定義循環池
NSMutableSet *recyclePool; MyCell *cell = [self.recyclePool anyObject]; if (cell) { // 從循環池內取出 [self.recyclePool removeObject:cell]; } else { // 創建 cell = [MyCell cell]; } // 超出屏幕再 add 進循環池
3、圖片緩存相關
參照《iOS:圖片相關》