需求背景
有這樣一個需求,有一個用來展示商品的列表,你可以從別的數據源添加過來,能添加當然就能刪除了,這時候就用到了UITableView/UICollextionView組或者cell的刪除,但在測試的過程中發現這里會出現crash,然后在一個夜深人靜的晚上安安靜靜的找了下原因,下面是我探究的結果來分享一下。
模擬一下
下面是一個簡單的demo來模擬這個問題,大致的思路如下:(沒用的代碼沒有粘貼出來 看關鍵點)
1、創建一個 tablewView 在Cell上添加一個刪除按鈕,給Cell設置一個index的標記。
2、點擊刪除回調 index 然后在數據源中按照 index 找到數據刪除掉。
3、執行 deleteSections 或者 deleteRows 來看看下面的簡單的代碼,看能看出問題嗎?
extension ViewController:UITableViewDelegate,UITableViewDataSource{
func numberOfSections(in tableView: UITableView) -> Int {
print("我來重新獲取 tableView SectionsNumber")
return array.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
print("我來重新獲取 tableView RowsNumber")
return 1
}
func tableView(_ tableView:UITableView,cellForRowAt indexPath:IndexPath) -> UITableViewCell {
print("我來重新獲取 cell")
let cell:TabCell = tableView.dequeueReusableCell(withIdentifier: "Identifier", for: indexPath) as! TabCell
let str = array[indexPath.section]
cell.index = indexPath
cell.setdata(str)
cell.block = {(index) in
print(index.section)
///
self.array.remove(at: index.section)
self.tabview.beginUpdates()
self.tabview.deleteSections(IndexSet.init(arrayLiteral: index.section), with: UITableView.RowAnimation.automatic)
self.tabview.endUpdates()
}
return cell
}
}
/// Cell 代碼
class TabCell: UITableViewCell {
var index:IndexPath?
var block:Block?
@objc func deleteClick() {
print("點擊事件之后的打印--")
self.block!(self.index!)
}
func setdata(_ str:Int) {
title.text = String(str)
}
}
下面是刪除的gif看看是否能順利的刪除完

刪除到一半的時候crash了!看看crash的日志,分析一下問題:

數組越界了!通過這點我們能分析出下面幾個結論:
1 、每次刪除的時候都會重新去獲取它的組數和組里面cell的個數。
2、不會重新走 cellForRowAt 所以我們給cell賦的index的值不會更新,所以刪除某一個cell的時候。它拿到的index還是最開始賦值給它的index,上面這兩點的原因造成了crash。
那分析到這一步,解決的辦法也就有了,你刪除完組或者cell之后重新reloaddata是能解決crash的,看看效果:

問題到了這里你可以說解決了,但也可以說沒解決。要是不介意UI效果(仔細看他們之間的區別),要是不介意性能的問題(數據量不會大)就可以這樣做,但像我這種比較追求UI效果,要是把App看做一個人的話那毫無疑問UI就是它的衣服,人靠衣裝嘛,那我們還有別的方式去解決的這問題嗎?
這時候我做了這樣一個嘗試,既然我們的index沒有發生改變,那數據源呢?我可以在它身上去做一些改變,在做改變之前我們還有一個問題需要去認識,說白了也是應為我們的index沒有及時刷新引起的。
要是你再這樣回調這個index做操作,然后刪除數組元素中的某一位置的元素,保證和剩下的section個數是一樣的,但是不刷新TableView ,會發生什么呢?看下面gif
我們刪除了 6、5、4 在回去刪除 8 的時候還是crash了,這時候我們的數據是這樣處理的 self.array.remove(at: 0) 按道理,刪除一組我就總數據源刪除0位置的元素,這時候剩下的section 和我們數據源的個數是對應的,發生crash的原因呢?不知道有沒有人這樣想,因為我們在返回組數的時候是采用了數據源的個數,它們倆之間是一一對應的,按道理似乎是不應該有問題的,但還是crash了,我們看看日志。
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'attempt to delete section 7, but there are only 5 sections before the update'
這句話的說的意思就是我們嘗試刪除 section 7 但在這之前我們的 numberSection 返回的組數卻是 5 ,這就產生了一個crash 原因前面說了。還是indexSection 沒法對應上的問題,或者說就是indexSection越界了。
我在網上有搜到這兩者之間不匹配的問題,比如你不刪除數據源,也就是沒有 self.array.remove(at: 0) ,你直接刪除一組,當然你返回組數的時候還是返回 self.array.count 這時候又會是什么問題呢?
'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of sections. The number of sections contained in the table view after the update (8) must be equal to the number of sections contained in the table view before the update (8), plus or minus the number of sections inserted or deleted (0 inserted, 1 deleted)
這里你再理解一下,你刪除之后按道理應該就剩7組了,但是在執行到返回組數的時候你的數據源返回的個數還是8,這里就是不匹配的問題,當然返回組個數是6也會crash,道理和我們這解釋的相同,要是有同類型的錯了就好好理解梳理一下,我們在做一些 update 操作的時候處理不好匹配問題也會經常遇到這個問題。
找一個方法解決
找一個辦法解決這個問題,我們前面有說要是reloaddata一次就解決問題了,那我們在reloaddata最重要的操作或者目的是什么呢?那就是給我們回調回來的 index 一個不越界的正常的值,我們從這點出發,我們在不執行reloadata的情況下回調一個正常的index應該也能解決問題,那有什么辦法回調一個正常的index呢?
其實也很簡單,我們賦給cell的index我們可以在執行完刪除之后自己重新組裝一次!那怎么組裝呢?這時候就要利用其我們傳給 cell 的model了,我們傳給cell 的model指向的還是我們數據源的model (swift引用類型。oc也是指針),並沒有重新賦值,這時候我們就可以在 model 里面寫一個 IndexPath 進去,然后在每一次刪除完之后我們自己操作在數據源中重新排列這個model中的indexPath ,在刪除點擊回調的時候直接回調這個model ,在選擇刪除的時候我們也刪除從model中獲取到的idnex不就解決了我們的問題了嘛!
上面就是解決我們這問題的思路。代碼其實也很簡單,簡單到不值得我們在寫出了。下面是我們自己項目中我執行這一段邏輯自己的代碼,幫助理清上面說的思路。
/// 刪除一個選中的商品
/// - Parameter index: index description
func deleteGoods(indexModel:PPOrderGoodListModel,tableView:UITableView) {
let index = indexModel.indexPath!
/// 部分退款 並且商品和憑證一對一的時候是按照組刪除的 別的情況是按照row刪除的
if self.refundType == .part && needAddGoods() {
/// 保證不會發生數組越界的情況
if self.refundChooseGoods.count >= (index.section + 1) {
self.refundChooseGoods.remove(at: index.section)
}
tableView.deleteSections([index.section], with: UITableView.RowAnimation.left)
self.resetIndexPath(false)
}else{
/// 保證不會發生數組越界的情況
if self.refundChooseGoods.count >= (index.row + 1) {
self.refundChooseGoods.remove(at: index.row)
}
///print("-----------count ",self.refundChooseGoods.count)
///print("-----------",index.section,"--------",index.row)
tableView.deleteRows(at: [index], with: UITableView.RowAnimation.left)
self.resetIndexPath(true)
}
}
/// 重新排列剩下的數據源的index,否則 crash
/// - Parameter updateRow: updateRow description
func resetIndexPath(_ updateRow:Bool) {
var index = 0
for model in self.refundChooseGoods {
if updateRow {
model.indexPath?.row = index
}else{
model.indexPath?.section = index
}
index += 1
}
}
最后看看我這樣做之后刪除組和刪除cel分別的效果:

