deleteSections & deleteRows 我踩的坑

2020-06-19 11:28:19 浏览数 (2)

需求背景


有这样一个需求,有一个用来展示商品的列表,你可以从别的数据源添加过来,能添加当然就能删除了,这时候就用到了UITableView/UICollextionView组或者cell的删除,但在测试的过程中发现这里会出现crash,然后在一个夜深人静的晚上安安静静的找了下原因,下面是我探究的结果来分享一下。

模拟一下


下面是一个简单的demo来模拟这个问题,大致的思路如下:(没用的代码没有粘贴出来 看关键点)

1、创建一个 tablewView 在Cell上添加一个删除按钮,给Cell设置一个index的标记。

2、点击删除回调 index 然后在数据源中按照 index 找到数据删除掉。

3、执行 deleteSections 或者 deleteRows 来看看下面的简单的代码,看能看出问题吗?

代码语言:javascript复制
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不就解决了我们的问题了嘛!

上面就是解决我们这问题的思路。代码其实也很简单,简单到不值得我们在写出了。下面是我们自己项目中我执行这一段逻辑自己的代码,帮助理清上面说的思路。

代码语言:javascript复制
    /// 删除一个选中的商品
    /// - 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分别的效果:

0 人点赞