一、基本用法

① 单个分区的集合视图

  • 效果如下:

RxSwift之UI控件UICollectionView扩展的使用-编程知识网

  • 示例代码:
// 定义布局方式以及单元格大小
let flowLayout = UICollectionViewFlowLayout()
flowLayout.sectionInset = UIEdgeInsets.init(top: 0, left: 5, bottom: 0, right: 5)
flowLayout.itemSize = CGSize(width: (SCREEN_WIDTH-50)/3.0, height: 70)// 创建集合视图
self.collectionView = UICollectionView(frame: self.view.frame,collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
self.view.addSubview(self.collectionView!)// 初始化数据
let items = Observable.just(["Swift","PHP","Ruby","Java","C++",])// 设置单元格数据(其实就是对 cellForItemAt 的封装)
items.bind(to: collectionView.rx.items) { (collectionView, row, element) inlet indexPath = IndexPath(row: row, section: 0)let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(row)\(element)"return cell}.disposed(by: disposeBag)

② 单元格选中事件响应

  • 如下所示,当点击某个单元格时将其索引位置,以及对应的标题打印出来:
选中项的indexPath为:[0, 0]
选中项的标题为:Swift
  • 如果业务代码直接放在响应方法内部,可以如下实现:
// 获取选中项的索引
collectionView.rx.itemSelected.subscribe(onNext: { indexPath inprint("选中项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)// 获取选中项的内容
collectionView.rx.modelSelected(String.self).subscribe(onNext: { item inprint("选中项的标题为:\(item)")
}).disposed(by: disposeBag)
  • 也可以在响应中调用外部的方法实现:
// 获取选中项的索引
collectionView.rx.itemSelected.subscribe(onNext: { [weak self] indexPath inprint("选中项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)// 获取选中项的内容
collectionView.rx.modelSelected(String.self).subscribe(onNext: {[weak self] item inprint("选中项的标题为:\(item)")
}).disposed(by: disposeBag)
  • 如果想要同时获取选中项的索引以及内容,可以如下实现:
Observable.zip(collectionView.rx.itemSelected, collectionView.rx.modelSelected(String.self)).bind { [weak self] indexPath, item inprint("选中项的indexPath为:\(indexPath)")print("选中项的标题为:\(item)")}.disposed(by: disposeBag)

③ 单元格取消选中事件响应

被取消选中项的indexPath为:[0, 4]
被取消选中项的的标题为:C++
  • 可以分别获取被取消项的索引及内容:
// 获取被取消选中项的索引
collectionView.rx.itemDeselected.subscribe(onNext: { [weak self] indexPath inprint("被取消选中项的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)// 获取被取消选中项的内容
collectionView.rx.modelDeselected(String.self).subscribe(onNext: {[weak self] item inprint("被取消选中项的的标题为:\(item)")
}).disposed(by: disposeBag)
  • 也可以同时获取:
Observable.zip(collectionView.rx.itemDeselected, collectionView.rx.modelDeselected(String.self)).bind { [weak self] indexPath, item inprint("被取消选中项的indexPath为:\(indexPath)")print("被取消选中项的的标题为:\(item)")}.disposed(by: disposeBag)

④ 单元格高亮完成后的事件响应

高亮单元格的indexPath为:[0, 3]
  • 示例代码:
// 获取选中并高亮完成后的索引
collectionView.rx.itemHighlighted.subscribe(onNext: { indexPath inprint("高亮单元格的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)

⑤ 高亮转成非高亮完成的事件响应

失去高亮的单元格的indexPath为:[0, 3]
  • 示例代码:
// 获取高亮转成非高亮完成后的索引
collectionView.rx.itemUnhighlighted.subscribe(onNext: { indexPath inprint("失去高亮的单元格的indexPath为:\(indexPath)")
}).disposed(by: disposeBag)

⑥ 单元格将要显示出来的事件响应

将要显示单元格indexPath为:[0, 0]
将要显示单元格cell为:<RxSwift.MyCollectionViewCell: 0x7fe0f1112490; baseClass = UICollectionViewCell; frame = (5 0; 126 70); layer = <CALayer: 0x6000006f1a40>>将要显示单元格indexPath为:[0, 1]
将要显示单元格cell为:<RxSwift.MyCollectionViewCell: 0x7fe0f0706be0; baseClass = UICollectionViewCell; frame = (144 0; 126 70); layer = <CALayer: 0x6000006f6360>>将要显示单元格indexPath为:[0, 2]
将要显示单元格cell为:<RxSwift.MyCollectionViewCell: 0x7fe0f1304420; baseClass = UICollectionViewCell; frame = (283 0; 126 70); layer = <CALayer: 0x6000006bd900>>将要显示单元格indexPath为:[0, 3]
将要显示单元格cell为:<RxSwift.MyCollectionViewCell: 0x7fe0f1304e40; baseClass = UICollectionViewCell; frame = (5 80; 126 70); layer = <CALayer: 0x6000006bd960>>将要显示单元格indexPath为:[0, 4]
将要显示单元格cell为:<RxSwift.MyCollectionViewCell: 0x7fe0f1113c50; baseClass = UICollectionViewCell; frame = (144 80; 126 70); layer = <CALayer: 0x6000006f18a0>>
  • 示例代码:
// 单元格将要显示出来的事件响应
collectionView.rx.willDisplayCell.subscribe(onNext: { cell, indexPath inprint("将要显示单元格indexPath为:\(indexPath)")print("将要显示单元格cell为:\(cell)\n")
}).disposed(by: disposeBag)

⑦ 分区头部或尾部将要显示出来的事件响应

// 分区头部、尾部将要显示出来的事件响应
collectionView.rx.willDisplaySupplementaryView.subscribe(onNext: { view, kind, indexPath inprint("将要显示分区indexPath为:\(indexPath)")print("将要显示的是头部还是尾部:\(kind)")print("将要显示头部或尾部视图:\(view)\n")
}).disposed(by: disposeBag)

二、RxDataSources

① 单分区的 CollectionView

  • 如下所示,要实现如下效果:

RxSwift之UI控件UICollectionView扩展的使用-编程知识网

  • 主视图控制器里的功能实现有如下两种写法:
    • 方式一:使用自带的 Section:
// 定义布局方式以及单元格大小
let flowLayout = UICollectionViewFlowLayout()
flowLayout.sectionInset = UIEdgeInsets.init(top: 0, left: 5, bottom: 0, right: 5)
flowLayout.itemSize = CGSize(width: (SCREEN_WIDTH-50)/3.0, height: 70)// 创建集合视图
self.collectionView = UICollectionView(frame: self.view.frame,collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
self.view.addSubview(self.collectionView!)// 初始化数据
let items = Observable.just([SectionModel(model: "", items: ["Swift","PHP","Python","Java","javascript","C#"])])// 创建数据源
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>>(configureCell: { (dataSource, collectionView, indexPath, element) inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(element)"return cell}
)// 绑定单元格数据
items.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
    • 方式二:使用自定义的 Section:
// 定义布局方式以及单元格大小
let flowLayout = UICollectionViewFlowLayout()
flowLayout.sectionInset = UIEdgeInsets.init(top: 0, left: 5, bottom: 0, right: 5)
flowLayout.itemSize = CGSize(width: (SCREEN_WIDTH-50)/3.0, height: 70)// 创建集合视图
self.collectionView = UICollectionView(frame: self.view.frame,collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
self.view.addSubview(self.collectionView!)// 初始化数据
let sections = Observable.just([MySection(header: "", items: ["Swift","PHP","Python","Java","javascript","C#"])])// 创建数据源
let dataSource = RxCollectionViewSectionedReloadDataSource<MySection>(configureCell: { (dataSource, collectionView, indexPath, element) inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(element)"return cell}
)// 绑定单元格数据
sections.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)// 自定义Section
struct MySection {var header: Stringvar items: [Item]
}extension MySection : AnimatableSectionModelType {typealias Item = Stringvar identity: String {return header}init(original: MySection, items: [Item]) {self = originalself.items = items}
}
  • 注意:RxDataSources 是以 section 来做为数据结构的,因此不管 collectionView 是单分区还是多分区,在使用 RxDataSources 的过程中,都需要返回一个 section 的数组。

② 多分区的 CollectionView

  • 如下所示,要实现如下效果:

RxSwift之UI控件UICollectionView扩展的使用-编程知识网

  • 使用自带的 Section:
// 定义布局方式以及单元格大小
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: (SCREEN_WIDTH-50)/3.0, height: 70)
flowLayout.headerReferenceSize = CGSize(width: self.view.frame.width, height: 40)// 创建集合视图
self.collectionView = UICollectionView(frame: self.view.frame,collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
// 创建一个重用的分区头
self.collectionView.register(MySectionHeader.self,forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,withReuseIdentifier: "Section")
self.view.addSubview(self.collectionView!)// 初始化数据
let items = Observable.just([SectionModel(model: "脚本语言", items: ["Python","javascript","PHP",]),SectionModel(model: "高级语言", items: ["Swift","C++","Java","C#"])])// 创建数据源
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>>(configureCell: { (dataSource, collectionView, indexPath, element) inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(element)"return cell},configureSupplementaryView: {(ds ,cv, kind, ip) inlet section = cv.dequeueReusableSupplementaryView(ofKind: kind,withReuseIdentifier: "Section", for: ip) as! MySectionHeadersection.label.text = "\(ds[ip.section].model)"return section
})// 绑定单元格数据
items.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)
  • 使用自定义的 Section:
// 定义布局方式以及单元格大小
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: (SCREEN_WIDTH-50)/3.0, height: 70)
flowLayout.headerReferenceSize = CGSize(width: self.view.frame.width, height: 40)// 创建集合视图
self.collectionView = UICollectionView(frame: self.view.frame,collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
// 创建一个重用的分区头
self.collectionView.register(MySectionHeader.self,forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader,withReuseIdentifier: "Section")
self.view.addSubview(self.collectionView!)// 初始化数据
let sections = Observable.just([MySection(header: "脚本语言", items: ["Python","javascript","PHP",]),MySection(header: "高级语言", items: ["Swift","C++","Java","C#"])])// 创建数据源
let dataSource = RxCollectionViewSectionedReloadDataSource<MySection>(configureCell: { (dataSource, collectionView, indexPath, element) inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(element)"return cell},configureSupplementaryView: {(ds ,cv, kind, ip) inlet section = cv.dequeueReusableSupplementaryView(ofKind: kind,withReuseIdentifier: "Section", for: ip) as! MySectionHeadersection.label.text = "\(ds[ip.section].header)"return section
})// 绑定单元格数据
sections.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)// 自定义Section
struct MySection {var header: Stringvar items: [Item]
}extension MySection : AnimatableSectionModelType {typealias Item = Stringvar identity: String {return header}init(original: MySection, items: [Item]) {self = originalself.items = items}
}

三、刷新集合数据

① 刷新数据

  • 如下所示:
    • 界面初始化完毕后,collectionView 默认会加载一些随机数据;
    • 点击右上角的刷新按钮,collectionView 会重新加载并显示一批新数据;
    • 为方便演示,每次获取数据不是真的去发起网络请求,而是在本地生成后延迟 2 秒返回,模拟这种异步请求的情况。

RxSwift之UI控件UICollectionView扩展的使用-编程知识网
RxSwift之UI控件UICollectionView扩展的使用-编程知识网

  • 示例代码:
// 定义布局方式以及单元格大小
let flowLayout = UICollectionViewFlowLayout()
flowLayout.itemSize = CGSize(width: (SCREEN_WIDTH-50)/3.0, height: 70)
flowLayout.headerReferenceSize = CGSize(width: self.view.frame.width, height: 40)// 创建集合视图
self.collectionView = UICollectionView(frame: CGRect.init(x: 0, y: 88, width: SCREEN_WIDTH, height: SCREEN_HEIGHT - 88),collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white
// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
self.view.addSubview(self.collectionView!)// 随机的表格数据
let randomResult = self.refreshButton.rx.tap.asObservable().startWith(()).flatMapLatest(getRandomResult).share(replay: 1)// 创建数据源
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, Int>>(configureCell: { (dataSource, collectionView, indexPath, element) inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(element)"return cell}
)// 绑定单元格数据
randomResult.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)// 获取随机数据
func getRandomResult() -> Observable<[SectionModel<String, Int>]> {print("正在请求数据......")let items = (0 ..< 5).map {_ inInt(arc4random_uniform(100000))}let observable = Observable.just([SectionModel(model: "S", items: items)])return observable.delay(2, scheduler: MainScheduler.instance)
}

② 防止集合视图多次刷新

  • flatMapLatest 的作用是:当在短时间内(上一个请求还没回来)连续点击多次“刷新”按钮,虽然仍会发起多次请求,但 collectionView 只会接收并显示最后一次请求,避免集合视图出现连续刷新的现象:
// 随机的表格数据
let randomResult = refreshButton.rx.tap.asObservable().startWith(()) .flatMapLatest(getRandomResult).share(replay: 1)
  • 也可以对源头进行限制下,即通过 throttle 设置个阀值(比如 1 秒),如果在 1 秒内有多次点击则只取最后一次,那么自然也就只发送一次数据请求:
// 随机的表格数据
let randomResult = refreshButton.rx.tap.asObservable().throttle(1, scheduler: MainScheduler.instance) // 在主线程中操作,1秒内值若多次改变,取最后一次.startWith(()) .flatMapLatest(getRandomResult).share(replay: 1)

③ 停止数据请求

  • 在实际项目中我们可能会需要对一个未完成的网络请求进行中断操作,比如切换页面或者分类时,如果上一次的请求还未完成就要将其取消掉。
  • 该功能简单说就是通过 takeUntil 操作符实现,当 takeUntil 中的 Observable 发送一个值时,便会结束对应的 Observable:
// 随机的表格数据
let randomResult = refreshButton.rx.tap.asObservable().startWith(()) //加这个为了让一开始就能自动请求一次数据.flatMapLatest{self.getRandomResult().takeUntil(self.cancelButton.rx.tap)}.share(replay: 1)

四、样式修改

  • 不管屏幕尺寸如何,collectionView 每行总是固定显示 4 个单元格,即单元格的宽度随屏幕尺寸的变化而变化,而单元格的高度为宽度的 1.5 倍:

RxSwift之UI控件UICollectionView扩展的使用-编程知识网

  • 示例代码:
// 定义布局方式
let flowLayout = UICollectionViewFlowLayout()// 创建集合视图
self.collectionView = UICollectionView(frame: self.view.frame,collectionViewLayout: flowLayout)
self.collectionView.backgroundColor = UIColor.white// 创建一个重用的单元格
self.collectionView.register(MyCollectionViewCell.self,forCellWithReuseIdentifier: "Cell")
self.view.addSubview(self.collectionView!)// 初始化数据
let items = Observable.just([SectionModel(model: "", items: ["Swift","PHP","Python","Java","C++","C#"])])// 创建数据源
let dataSource = RxCollectionViewSectionedReloadDataSource<SectionModel<String, String>>(configureCell: { (dataSource, collectionView, indexPath, element) inlet cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell",for: indexPath) as! MyCollectionViewCellcell.label.text = "\(element)"return cell}
)// 绑定单元格数据
items.bind(to: collectionView.rx.items(dataSource: dataSource)).disposed(by: disposeBag)// 设置代理
collectionView.rx.setDelegate(self).disposed(by: disposeBag)// 设置单元格尺寸
func collectionView(_ collectionView: UICollectionView,layout collectionViewLayout: UICollectionViewLayout,sizeForItemAt indexPath: IndexPath) -> CGSize {let width = collectionView.bounds.widthlet cellWidth = (width - 30) / 4                         // 每行显示4个单元格return CGSize(width: cellWidth, height: cellWidth * 1.5) // 单元格宽度为高度1.5倍
}