1. 程式人生 > >deleteSections & deleteRows 我踩得坑

deleteSections & deleteRows 我踩得坑

 

需求背景


 

      有這樣一個需求,有一個用來展示商品的列表,你可以從別的資料來源新增過來,能添加當然就能刪除了,這時候就用到了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分別的效果:

 

                                                      

&n