前言
UITableView
是開發(fā)中常用的列表組件,它封裝了UITableViewCell
的復用等功能,但是這不是我們今天討論的重點.今天要討論的是UITableView
的列表動畫,其中包括
1.動態(tài)插入元素
2.動態(tài)刪除元素
3.批量插入和刪除元素
4.動態(tài)修改cell內item的樣式
5.動態(tài)改變cell的高度
動態(tài)插入和刪除元素
UITableView
提供了以下方法來進行插入刪除和刷新的操作
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)deleteSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)reloadSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
- (void)insertRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation: (UITableViewRowAnimation)animation;
- (void)deleteRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation: (UITableViewRowAnimation)animation;
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
需要注意的是在進行插入和刪除動畫前,記得要更改數據源.數據源必須和編輯后的元素保持一致.
這里引出一個異常*** Assertion failure in -[UITableView _endCellAnimationsWithContext:]
,當進行了insert
,delete
等操作,但是和datasource
不一致時,就會出現該異常.
以上操作都是獨立執(zhí)行的(也就是修改一次數據源,執(zhí)行一次insert
或delete
),這里我們遇到一個問題,如果我想批量操作列表,舉例 : 刪除section0
的row0
,同時想刪除整個section2
,與此同時我還想在section2
的row3
添加某個元素,應該怎么做呢?
UITableView
提供了批量操作delete
,insert
和reload
的方法,并且按照特定的順序規(guī)則動態(tài)的執(zhí)行這些操作,這個順序的規(guī)范下面我們會提到,繼續(xù)往下看.
批量刪除和插入動畫
如果想批量處理insert
,delete
和reload
方法,這里我們要引入一個動畫塊
在這兩個方法內執(zhí)行的操作,都會被包裝好,統(tǒng)一交由UITableView
處理.在動畫塊執(zhí)行完成之后,tableview
還是回去datasource
和delegete
來獲取數據,所以就說明了為什么一定要在進行插入和刪除動畫前,記得要更改數據源.
下面的效果是我們刪除了section0
的row1
,同時刪除了整個section1
,因為調用了
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationLeft];
所以只能封裝在動畫塊內執(zhí)行,即
//修改數據源
[self.tempArray[0] removeObjectAtIndex:1];
[self.tempArray removeObjectAtIndex:1];
//執(zhí)行動畫
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:1] withRowAnimation:UITableViewRowAnimationLeft]
[self.tableView endUpdates];
tips:iOS11
以后提供了方法performBatchUpdates(_:completion:)
來代替上面的beginUpdates
和endUpdates
批量刪除和插入的執(zhí)行順序
在-beginUpdates
和-endUpdates
代碼塊內的更新指令,并不是安裝他們的添加順序執(zhí)行的,我們看下面的例子
1. [tableview beginUpdates];
2. [tableview insertRowsAtIndexPaths:insertIndexPaths withRowAnimation:UITableViewRowAnimationRight];
3. [tableview deleteRowsAtIndexPaths:deleteIndexPaths withRowAnimation:UITableViewRowAnimationFade];
4. [tableview endUpdates];
插入和刪除哪個先執(zhí)行呢?
你一定以為deleteRowsAtIndexPaths:withRowAnimation:
是在insertRowsAtIndexPaths:withRowAnimation:
以后執(zhí)行的,其實不然.
UITableView
會延遲所有的插入操作,先執(zhí)行列表的刪除操作.
除此之外,UITableView
對update block
內調動reload
方法也不是順序執(zhí)行的,當在update
的代碼塊內調用reload
方法時,會延遲列表的插入和刪除操作,首先執(zhí)行reload
方法.
先執(zhí)行刪除,然后再執(zhí)行插入,了解這個概念非常重要
批量刪除和插入可能會出現意想不到的后果
這段要介紹一下delete
和insert
內部實現,上面我們已經了解了先執(zhí)行刪除,然后再執(zhí)行插入,但是他們操作的數據源是不同的,這段可能有點繞,我會借助一些圖形和示例進行解釋,請認真看下去.
以下的討論都是在beginUpdates
和endUpdates
動畫塊內執(zhí)行的前提下,這個前提我們不再贅述.首先看一下官方文檔的描述 :
-
delete
和reload
操作,針對的數據源是在所有操作未進行前的原始數據源. -
insert
操作,針對的數據源是在所有操作執(zhí)行后的結果數據源.
插入是針對結果數據源
我們說過: insert
操作,針對的數據源是在所有操作執(zhí)行后的結果數據源.
如果有個數組,內部元素為遞增整數[1...8],執(zhí)行下面代碼,數據源結果和動畫執(zhí)行結果分別是什么呢?
[self.tempArray insertObject:@100 atIndex:3];
[self.tempArray insertObject:@200 atIndex:5];
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:3 inSection:0]] withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView insertRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:5 inSection:0]] withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView endUpdates];
先看數據源的變化: [1, 2, 3,100, 4, 200, 5, 6, 7, 8]
執(zhí)行動畫的結果: [1, 2, 3,100, 4, 200, 5, 6, 7, 8]
我們看到插入動畫的執(zhí)行結果,和編輯后的
self.tempArray
是一樣的,即 insert
操作,針對的數據源是在所有操作執(zhí)行后的結果數據源.那么什么叫原始數據源,什么又是
delete
和reload
操作,針對的數據源是在所有操作未進行前的原始數據源.往下看
刪除是針對原始數據源
先進行個小總結: 同時對一個數組的多個刪除操作會導致刪除的動畫執(zhí)行結果和數據源不一致.原因是數組內index
的計算數據源和動畫的計算方式是不一樣的.
所以請盡量避免同時對一個數組進行多次刪除操作,具體我們來解釋下!
如果有個數組,內部元素為遞增整數[1...8]
,執(zhí)行結果應該是什么呢?
[self.tempArray removeObjectAtIndex:1];
[self.tempArray removeObjectAtIndex:5];
[self.tableView beginUpdates];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:5 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView endUpdates];
這里我們順便回顧一下上面代碼塊內執(zhí)行順序
的介紹先執(zhí)行刪除,然后再執(zhí)行插入,即beginUpdates
內的insert
代碼,放在delete
前面和后面對結果都不會有任何影響.(對數組的操作,即tempArray
的增加和刪除還是按照先后順序執(zhí)行的,我們說的順序只是代碼塊內的動畫執(zhí)行順序)
先看數據源的改變: 變?yōu)榱?code>[1, 3, 4, 5, 6, 8]
但是我們看一下動畫的執(zhí)行結果: [1, 3, 4, 5, 7, 8]
WHAT?居然和數據源不一樣?別急,我們來解釋下
還記得上面說過: delete
和reload
操作,針對的數據源是在所有操作未進行前的原始數據源.這個數據源,是在執(zhí)行動畫塊前,在tableview
當前展示的原始的數據源[1, 2, 3, 4, 5, 6, 7, 8]
,也可以理解為tableview
已經緩存好的數據源.
我們知道的是動畫的刪除操作,并不會真正的修改數據源,所以執(zhí)行以下代碼,都是從[1, 2, 3, 4, 5, 6, 7, 8]
里面去對應index,也就是row1
和row5
,所以結果變?yōu)榱?code>[1, 3, 4, 5, 7, 8]
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:1 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView deleteRowsAtIndexPaths:@[[NSIndexPath indexPathForRow:5 inSection:0]] withRowAnimation:UITableViewRowAnimationFade];
這就解釋了上面的說法: delete
和reload
操作,針對的數據源是在所有操作未進行前的原始數據源.
最終還是使用數據源
上面的操作,列表的展示結果和數據源不一致,我們上面說過,動畫塊執(zhí)行結束后,UITableView
還是去datasource
和deletgate
去獲取數據
還記得UITableViewCell
的復用機制嗎?當我們滑動界面時,如果展示錯誤的cell-----7
不在當前可視屏幕范圍內時,它會進入復用池,被其他cell
復用,當cell-----7
再次回到屏幕時,它會通過調用cellForRowAtIndexPath:(NSIndexPath *)indexPath
方法來獲取數據,這時的使用的數據源self.tempArray
是[1, 3, 4, 5, 6, 8]
,所以你猜發(fā)生了什么?沒錯,cell-----7
變成了cell-----6
,和self.tempArray
的數據一致了.
這樣界面的變化,會讓用戶非常的困惑,所以請盡量避免同時對一個數組進行多次刪除操作
動態(tài)改變cell的樣式
UITableView
有編輯模式(edit mode
)和普通模式(normal mode
)兩種模式,可以通過方法 setEditing:animated:來切換這兩種模式.
當tableview
接收setEditing:animated:
方法時,會觸發(fā)每個可視UITableViewCell
的同名方法setEditing:animated:
.先看下效果圖
修改做了以下事情:
- titleLabel左移動
- switch隱藏
- ...
實現方法:
- 進入編輯模式
[self.tableView setEditing:YES animated:YES];
- 在
UITableViewCell
的方法- (void)setEditing:(BOOL)editing animated:(BOOL)animated
內修改frame
- (void)setEditing:(BOOL)editing animated:(BOOL)animated{
[super setEditing:editing animated:animated];
self.titleLabel.frame.origin.x -= 12;
}
不需要自己添加任何animation的代碼,搞定!
動態(tài)改變cell的高度
-beginUpdates
和-endUpdates
除了用作批量處理動畫外,還可以用于動態(tài)刷新某些cell的樣式.
可以通過執(zhí)行-beginUpdates
和-endUpdates
,即不在動畫塊內添加任何代碼,來動態(tài)改變cell的高度.
具體實現:
1.在heightForRowAtIndexPath:(NSIndexPath *)indexPath
進行設置,_isEdit是自己管理的一個狀態(tài)變量
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (_isEdited && indexPath.row == 0) {
return 100;
}
return 44;
}
2.通過-beginUpdates
和-endUpdates
調用更新
- (void)updateHeight {
_isEdited = YES;
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
搞定!