抛出问题
在使用RxTableViewSectionedAnimatedDataSource配置数据tableView的数据源时,在对数组增加Item时会遇到 Duplicate item的error并导致程序崩溃。

复现崩溃的示例代码
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class ViewController: UIViewController {
let bag = DisposeBag()
@IBOutlet weak var addItem: UIBarButtonItem!
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
let initialArray = [String]()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
let ob = self.addItem.rx.tap.map {"新增加一个Item"}
.scan(initialArray) { (arr, item) -> [String] in
var _arr = arr
_arr.append(item)
return _arr
}.map { (arr) -> [AnimatableSectionModel<String,String>] in
return [AnimatableSectionModel.init(model: "", items: arr)]
}
let dataSource = RxTableViewSectionedAnimatedDataSource<AnimatableSectionModel<String,String>>.init(animationConfiguration: AnimationConfiguration.init(insertAnimation: .fade, reloadAnimation: .automatic, deleteAnimation: .automatic), configureCell: { (dataSource, tv, indexPath, ele) -> UITableViewCell in
let cell = tv.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = ele
return cell
})
ob.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: bag)
}
}
以上代码第二次点击
addItem后,发生上述崩溃。
问题探索
百度了很多没有答案,在
github上找到了相关issue
Snip20200126_2.png
回答中提到了在tableView的dataSource中item不能有相同的id,熟悉tableview的iOSer都知道除了tableview的CellReuseIdentifier之外,就没有涉及到和id相关的东西了,但是显然CellReuseIdentifier和崩溃并没有联系。问题到这里也就陷入了瓶颈。
我们将
AnimatableSectionModel中的items用模型替代再来试试,我们定一个DemoModel,用模型驱动tableViewCell的显示,代码如下
class DemoModel {
var title:String
init(_ title:String) {
self.title = title
}
}
这个时候问题发生了,将自定义的
Model作为数据源无法编译通过

编译器提示我们这个自定义的Model必须遵循
IdentifiableType协议
我们再来看看这个IdentifiableType协议中有什么
public protocol IdentifiableType {
associatedtype Identity: Hashable
var identity : Identity { get }
}
看到这里,相信大家心里都有答案了,回答中的id应该是遵循了
IdentifiableType协议的对象(结构体)中的identity属性。我们让DemoModel遵循IdentifiableType协议,并实现identity这个计算属性的get方法。
class DemoModel : IdentifiableType{
var title:String
var id:String = "" // 为每个DemoModel定义一个唯一标识符
var identity: String { return id } // IdentifiableType协议方法,返回一个唯一标识符
init(_ title:String,id:String) {
self.title = title
self.id = id
}
}
遵循了协议后,依然报错,不过这次的报错是没有遵守
Equatable协议

我们继续让
DemoModel遵循Equaltalbe协议 并实现 == 方法
class DemoModel : IdentifiableType,Equatable{
var title:String
var id:String = "" // 为每个DemoModel定义一个唯一标识符
var identity: String { return id } // IdentifiableType协议方法,返回一个唯一标识符
init(_ title:String,id:String) {
self.title = title
self.id = id
}
// 以id判断两个不同的DemoModel对象是否相等
static func == (lhs: DemoModel, rhs: DemoModel) -> Bool {
lhs.id == rhs.id
}
}
问题解决
编译通过!捋一下思路 :
RxTableViewSectionedAnimatedDataSource中的items不允许有两个相同的item,否则就会崩溃,那么RXSwift是如何判断两个item是否相同呢?它强制要求我们的item遵循IdentifiableType协议返回一个唯一标识符,再强制我们遵循Equatable协议,通过比较两个唯一标识符是否相等来确定是否为同一个item。如果是,那就抛出异常,至于AnimatedDataSource中的items为什么不允许有重复的item,这就不得而知了,我猜测这与tableview的动画有关。
回头再看一下崩溃的示例代码,我们直接用
String作为Items数组元素,是因为苹果已经让String遵循了IdentifiableType、Equatable两个协议,String的identity属性返回的是String的本身,这意味着如果两个String的内容相等,那么RXSwift就会认为这两个item就相等并抛出异常,也就出现了文章开头的情况
正确代码
import UIKit
import RxSwift
import RxCocoa
import RxDataSources
class DemoModel : IdentifiableType,Equatable{
var title:String
var id:String = "" // 为每个DemoModel定义一个唯一标识符
var identity: String { return id } // IdentifiableType协议方法,返回一个唯一标识符
init(_ title:String,id:String) {
self.title = title
self.id = id
}
// 以id判断两个不同的DemoModel对象是否相等
static func == (lhs: DemoModel, rhs: DemoModel) -> Bool {
lhs.id == rhs.id
}
}
class ViewController: UIViewController {
let bag = DisposeBag()
@IBOutlet weak var addItem: UIBarButtonItem!
@IBOutlet weak var tableView: UITableView!
var incrementItemID = 0
override func viewDidLoad() {
super.viewDidLoad()
let initialArray = [DemoModel]()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
let ob = self.addItem.rx.tap.map {DemoModel.init("新的Item",id: "\(self.incrementItemID)")}
.scan(initialArray) { (arr, item) -> [DemoModel] in
self.incrementItemID += 1
var _arr = arr
_arr.append(item)
return _arr
}.map { (arr) -> [AnimatableSectionModel<String,DemoModel>] in
return [AnimatableSectionModel.init(model: "", items: arr)]
}
let dataSource = RxTableViewSectionedAnimatedDataSource<AnimatableSectionModel<String,DemoModel>>.init(animationConfiguration: AnimationConfiguration.init(insertAnimation: .fade, reloadAnimation: .automatic, deleteAnimation: .automatic), configureCell: { (dataSource, tv, indexPath, ele) -> UITableViewCell in
let cell = tv.dequeueReusableCell(withIdentifier: "cell")!
cell.textLabel?.text = ele.title
return cell
})
ob.bind(to: tableView.rx.items(dataSource: dataSource)).disposed(by: bag)
}
}
