需求背景


有这样一个需求,有一个用来展示商品的列表,你可以从别的数据源添加过来,能添加当然就能删除了,这时候就用到了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分别的效果:

              

deleteSections & deleteRows 我踩得坑的更多相关文章

  1. 从零开始学 Java - Spring 支持 CORS 请求踩的坑

    谁没掉进过几个大坑 记得好久之前,总能时不时在某个地方看到一些标语,往往都是上面一个伟人的头像,然后不管是不是他说的话,下面总是有看起来很政治正确且没卵用的屁话,我活到目前为止,最令我笑的肚子痛得是下 ...

  2. webuploader插件,我踩得坑

    我在目前的公司做的项目要么是原生写法去做项目,要么就是vue+webpack做项目,但是vue这部分只是用了模板template,vue其他的都没用. 有一个项目需要做上传图片的功能,老大扔给我一个插 ...

  3. 谈谈调用腾讯云【OCR-通用印刷体识别】Api踩的坑

    一.写在前面 最近做项目需要用到识别图片中文字的功能,本来用的Tesseract这个写的,不过效果不是很理想. 随后上网搜了一下OCR接口,就准备使用腾讯云.百度的OCR接口试一下效果.不过这个腾讯云 ...

  4. Asp.Net Core中使用Swagger,你不得不踩的坑

    很久不来写blog了,换了新工作后很累,很忙.每天常态化加班到21点,偶尔还会到凌晨,加班很累,但这段时间,也确实学到了不少知识,今天这篇文章和大家分享一下:Asp.Net Core中使用Swagge ...

  5. python绘图踩的坑

    踩的坑 pyecharts安装地图包 pip install echarts-countries-pypkg 报错Unknown or unsupported command 'install' 这可 ...

  6. 使用CCNode作为容器容易踩的坑

    Cocos2dx中CCNode经常作为一个父容器,里面装一些UI控件,最后组成一个复杂的自定义的UI控件,但是在使用别人的自定义控件和自己写自定义问题的时候会踩一些坑. 首先拿到一个自定义的UI控件一 ...

  7. java基础不牢固容易踩的坑

    java基础不牢固容易踩的坑 经过一年java后端代码以及对jdk源码阅读之后的总结,对java中一些基础中的容易忽略的东西写下来,给偏爱技术热爱开源的Coder们分享一下,避免在写代码中误入雷区. ...

  8. Ubuntu中安装FTP 服务器自己踩得坑

    12点多了,擦!做个码农真不容易呀! 系统:Ubuntu16.04 安装:FTP 步骤: 1.不管有没有一上来我先卸载: sudo apt-get purge vsftpd 2.再安装:sudo ap ...

  9. python——pyinstaller踩的坑 UnicodeDecodeError

    程序本身运行没任何毛病,奈何用pyinstaller -w xx.py的时候提示——UnicodeDecodeError: 'ascii' codec can't decode byte 0xb3 i ...

随机推荐

  1. 原生js实现简单的下拉加载

    #获取当前滚动条的高度document.documentElement.scrollTop #获取当前窗口的高度 document.documentElement.clientHeight #获取当前 ...

  2. ETCD:单机单节点

    原文地址:Setting up local clusters 设置单节点集群 对于测试环境与开发环境,最快速与简单的方式是配置一个本地集群.对于生产环境,参考集群部分. 本地单节点集群 启动一个集群 ...

  3. 看完这篇还不会用Git,那我就哭了!

    你使用过 Git 吗?也许你已经使用了一段时间,但它的许多奥秘仍然令人困惑. Git 是一个版本控制系统,是任何软件开发项目中的主要内容.通常有两个主要用途:代码备份和代码版本控制.你可以逐步处理代码 ...

  4. 痞子衡嵌入式:恩智浦机器视觉模块OpenMV-RT那些事(1)- 初体验

    大家好,我是痞子衡,是正经搞技术的痞子.本系列痞子衡给大家介绍的是机器视觉模块OpenMV-RT初体验. 近些年机器视觉应用一直是个很火的方向,想象一下机器如果能长上"眼睛",是不 ...

  5. 使用centos7安装PXE教程

    PXE是一种电脑无盘(即没有硬盘)技术. 预启动执行环境(PXE)指的是那些使得IBM兼容计算机(经常是运行Windows系统)不需要硬盘或是启动软盘就能启动的方法. 通俗点讲就是配置好PXE以后可以 ...

  6. php 开山篇

    由韩顺平老师讲解的 php课程体系 初级课程只能是静态页面开发,不能动态的使用,只是一个界面 学完之后脑海中 应该有的体系~

  7. Python内置类属性,元类研究

    Python内置类属性 我觉得一切都是对象,对象和元类对象,类对象其实都是一样的,我在最后进行了证明,但是只能证明一半,最后由于元类的父类是type,他可以阻挡对object属性的访问,告终 __di ...

  8. 谷歌地图 API 开发之获取坐标以及街道详情

    自己的项目中有获取当前点击的坐标经纬度或者获取当前街道的信息的需求.估计这个对于新手来说,还是比较麻烦的,因为从官网上找这个也并不是很好找,要找好久的,运气好的可能会一下子找到. 献上自己写的测试案例 ...

  9. 性能达到原生 MySQL 七倍,华为云 Taurus 技术解读【华为云技术分享】

    版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https://blog.csdn.net/devcloud/article/detai ...

  10. SpringCache自定义过期时间及自动刷新

    背景前提 阅读说明(十分重要) 对于Cache和SpringCache原理不太清楚的朋友,可以看我之前写的文章:Springboot中的缓存Cache和CacheManager原理介绍 能关注Spri ...