抛出问题
在使用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
回答中提到了在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)
}
}