本篇为续篇TableProvider
上片 TableProvider,我们对 tableViewController进行抽象封装,将其修改成一个以数据驱动,具备table基础功能,兼顾嵌套滚动,代理响应的插件,本篇同样也是,对 collection 做一个基础的封装,将其抽出一个插件.
封装
定义一个 CollectionProvider 协议
protocol CollectionProvider: UIViewController {
associatedtype DataType: DiffableJSON
var customFlowlayout: UICollectionViewFlowLayout? { get }
var scrollDirection: UICollectionView.ScrollDirection { get }
var collectionViewController: CollectionViewController<DataType> { get }
var collectionView: CollectionView { get }
var list:[DataType] { get }
}
并且为其拖展属性
fileprivate var collectionViewControllerKey: UInt8 = 0
extension CollectionProvider {
var customFlowlayout: UICollectionViewFlowLayout? {
nil
}
var scrollDirection: UICollectionView.ScrollDirection {
.vertical
}
var collectionViewController: CollectionViewController<DataType> {
get {
associatedObject(&collectionViewControllerKey) {
let layout = customFlowlayout ?? ListCollectionViewLayout(stickyHeaders: false, scrollDirection: scrollDirection, topContentInset: 0, stretchToEdge: false)
let vc = CollectionViewController<DataType>(layout: layout)
return vc
}
}
set {
setAssociatedObject(&collectionViewControllerKey, newValue)
}
}
var collectionView: CollectionView {
collectionViewController.collectionView
}
var adapt: ListAdapter {
collectionViewController.adapt
}
var list:[DataType] {
get {
collectionViewController.list
}
set {
collectionViewController.list = newValue
}
}
}
项目中我使用 Collection是基于 IGListKit 框架封装,这是一种数据驱动,支持局部刷新的 Collection UI 框架,由Instagram团队开发开源,我强烈建议每个 iOS 开发都去使用,它的 diff 算法,局部刷新,数据驱动,每一项都令人惊叹.
初始化,MultiScroll 啥的配置就没必要讲了,就是普通配置的那一套,这里着重讲一下 dataSource 和 delegate 的配置.
下面展示的是 CollectionController 的封装
初始化
首先是遵循的协议,核心是 IGListKit 的代理.
class CollectionViewController<T: DiffableJSON>: UIViewController,
UIScrollViewDelegate,
UICollectionViewDelegateFlowLayout,
ListAdapterDataSource,
ListAdapterDelegate,
DZNEmptyDataSetDelegate,
DZNEmptyDataSetSource,
ScrollStateful,
UIGestureRecognizerDelegate
初始化这里我提供了一个方法,抛出去几个属性,都是 IG 的
private init() {
super.init(nibName: nil, bundle: nil)
configCollection()
}
internal required init?(coder: NSCoder) {
super.init(coder: coder)
configCollection()
}
convenience init(workingRangeSize: Int = 3,
layout: UICollectionViewLayout = ListCollectionViewLayout(stickyHeaders: false, scrollDirection: UICollectionView.ScrollDirection.vertical, topContentInset: 0, stretchToEdge: false)) {
self.init()
self.workingRangeSize = workingRangeSize
self.collectionViewLayout = layout
}
初始化collection 和 adapt
private func configCollection() {
let c = CollectionView(frame: .screenBounds, collectionViewLayout: collectionViewLayout)
c.panDelegate = self
collectionView = c
c.alwaysBounceVertical = true
c.dataSource = nil
c.delegate = self
c.alwaysBounceHorizontal = false
c.backgroundColor = .clear
c.emptyDataSetSource = self
c.emptyDataSetDelegate = self
adapt = ListAdapter(updater: listAdapterUpdater ?? ListAdapterUpdater(), viewController: self, workingRangeSize: workingRangeSize)
adapt.collectionView = c
adapt.dataSource = self
adapt.scrollViewDelegate = self
adapt.delegate = self
c.add(to: self.view)
}
configCollection有个点需要注意下,我在init 时调用 configCollection,如果这时候去调用到 self.view,那么他会先走 子类的 viewDidLoad,而又由于我的初始化中 adapt 为 强解包类型,如果不在调用 self.view 前将所有的值赋值完成,就会发生崩溃.下面放张图你们就懂了.


所以我吧 self.view 的调用放在了最后一行
c.add(to: self.view)
IGListKit代理实现
func objects(for listAdapter: ListAdapter) -> [ListDiffable] {
log("list: \(list.count)")
return list
}
func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController {
let controller = collectionView.sectionController(for: object)
controller.nextResponder = self
return controller
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
(list[safe: indexPath.row] as? LayoutCachable)?.cellSize ?? .zero
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
if let delegate = self.parent as? UICollectionViewDelegateFlowLayout,
delegate.responds(to: #selector(collectionView(_:didSelectItemAt:))) {
delegate.collectionView?(collectionView, didSelectItemAt: indexPath)
return
}
if let model = list[safe: indexPath.row] {
selectCellInput.send(value: model)
return
}
}
复用 sectionController 这里的写法跟tableProvider一模一样,内部维护了一个registeredSectionControllers.
final func sectionController<T: Any>(for model: T) -> ListSectionController {
if let controller = sectionControllerForModel?(model) {
return controller
}
if let controllerClosure = controllerMapper(for: model)?.controllerClosure {
return controllerClosure()
}
return ListSingleSectionController<T, EmptyCollectionCell>()
}
class SectionControllerMapper {
fileprivate(set) var modelClass: AnyClass
var controllerClosure: SectionControllerClosure
init(modelClass: AnyClass, controllerClosure: @escaping SectionControllerClosure) {
self.modelClass = modelClass
self.controllerClosure = controllerClosure
}
}
var registeredSectionControllers: [SectionControllerMapper] {
get {
guard let controllers = property(for: &Keys.UICollectionView.registeredSectionControllers) as? [SectionControllerMapper] else {
let controllers = [SectionControllerMapper]()
setProperty(for: &Keys.UICollectionView.registeredSectionControllers, controllers)
return controllers
}
return controllers
}
set {
setProperty(for: &Keys.UICollectionView.registeredSectionControllers, newValue)
}
}
final func controllerMapper(for model: Any) -> SectionControllerMapper? {
guard let model = model as? NSObjectProtocol else {
return nil
}
var matches = [SectionControllerMapper]()
for k in registeredSectionControllers {
if model.isKind(of: k.modelClass) {
matches.append(k)
}
}
return matches.sorted { $0.modelClass.isSubclass(of: $1.modelClass) }.first
}
这里也提供了几个 register 方法,省去写block 的烦恼
final func register<T: ListSectionController, O: NSObjectProtocol>(controller: T.Type, for model: O.Type) {
if let model = model as? NSObject.Type {
registeredSectionControllers += [SectionControllerMapper(modelClass: model.classForCoder(), controllerClosure: { () -> ListSectionController in
return controller.init()
})]
}
}
final func register<T: UICollectionViewCell & ListBindable, O: NSObjectProtocol>(singleCell: T.Type, for model: O.Type) {
register(controller: ListSingleSectionController<O, T>.self, for: O.self)
}
final func register<O: NSObjectProtocol>(closure: @escaping SectionControllerClosure, for model: O.Type) {
if let model = model as? NSObject.Type {
registeredSectionControllers += [SectionControllerMapper(modelClass: model.classForCoder(), controllerClosure: closure)]
}
}
empty
// MARK: - --------------------------------------Empty
func emptyView(for listAdapter: ListAdapter) -> UIView? {
nil
}
func customView(forEmptyDataSet scrollView: UIScrollView!) -> UIView! {
guard let emptyView = self.emptyView else {
warning("You should set a value for `emptyView` to display empty placeholder, maybe use `EmptyView` to implement is easier.")
return UIView()
}
return emptyView
}
func emptyDataSetShouldDisplay(_ scrollView: UIScrollView!) -> Bool {
self.internalShouldDisplayEmptyView
}
func emptyDataSetShouldAllowScroll(_ scrollView: UIScrollView!) -> Bool {
true
}
func verticalOffset(forEmptyDataSet scrollView: UIScrollView!) -> CGFloat {
scale(self.emptyViewVerticalOffset)
}
func refreshEmptyStatus() {
if self.shouldDisplayEmptyView {
self.internalShouldDisplayEmptyView = list.isEmpty
} else {
self.internalShouldDisplayEmptyView = false
}
}
empty 设置之前在 tableProvider 中没有提过,后来加上了,这里就顺便展示一下,用的DZNEmptyDataSetSource,基本也是大家数值的, emptyView这个属性,推荐大家做成单例,或者抽象工厂模式.
var emptyView: UIView? = EmptyViewInstance.shared.default
struct EmptyViewInstance {
static var shared = EmptyViewInstance()
var `default`: EmptyView {
EmptyView().emptyImage(R.image.empty_feed()).tips("暂无数据")
}
var blackList: EmptyView {
EmptyView().emptyImage(UIImage(named: "general_empty_blacklist")).tips("还没有黑名单哦~")
}
var postNearby: EmptyView {
EmptyView().emptyImage(R.image.empty_locating()).tips("获取位置信息失败\n请到 “设置>隐私>定位服务” 中开启定位服务").then({
$0.retryButtonTitle = "开启定位"
$0.retryButtonWidth = 170
})
}
///师徒-徒弟
var student: EmptyView {
EmptyView().emptyImage(R.image.empty_feed()).tips("好惨一\(Const.friend),连个徒弟都没有~").then({
$0.retryButtonTitle = "去收个小徒弟"
$0.retryButtonWidth = 122
})
}
}
EmptyView 这里其实可以延伸出来如何自定义一个控件,包括自动布局的intrinsicContentSize,以及 frame 布局,yoga布局的 sizeToFit,有时间再说吧~
CollectProvider 整体的结构差不多也就这样,然后讲讲 ig 对 model层以及 sectionController 的配置.
SectionController
SectionController 是 IGListKit最重要的部分,但是其本身的 ListSectionController 使用略微有些麻烦,因为其数据是通过代理提供的,这里通过封装抽出一层抽象层 ListSingleSectionController
无封装的使用是这样的代码
class DiscoverItemController: ListSectionController {
var model: DiscoverItemModel?
override func numberOfItems() -> Int { 1 }
override func sizeForItem(at index: Int) -> CGSize {
guard let model = model else { return .zero }
if let cache = model as? LayoutCachable {
return cache.cellSize
} else if let cache = DiscoverItemCell.self as? LayoutCachable.Type {
return cache.cellSize
}
return .zero
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
let cell = collectionContext?.dequeueReusableCell(of: DiscoverItemCell.self, for: self, at: index) as! DiscoverItemCell
if let bind = cell {
bind.bindViewModel(self.model as Any)
}
return cell
}
override func didUpdate(to object: Any) {
guard let model = object as? DiscoverItemModel else { return }
self.model = model
}
}
我们抽出一个抽象层,自动绑定 model 与 cell
class ListSingleSectionController<Model, Cell: UICollectionViewCell>: ListSectionController {
override init() {
super.init()
}
var model: Model?
func layoutConfig(inset: UIEdgeInsets = .zero, minimumLineSpacing: CGFloat = 0, minimumInteritemSpacing: CGFloat = 0) -> Self {
self.inset = inset
self.minimumLineSpacing = minimumLineSpacing
self.minimumInteritemSpacing = minimumInteritemSpacing
return self
}
override func numberOfItems() -> Int { 1 }
override func sizeForItem(at index: Int) -> CGSize {
if let model = model as? LayoutCachable {
return model.cellSize
} else if let model = Model.self as? LayoutCachable.Type {
return model.cellSize
} else if let model = Cell.self as? LayoutCachable.Type {
return model.cellSize
} else {
return .zero
}
}
override func cellForItem(at index: Int) -> UICollectionViewCell {
var cell: UICollectionViewCell?
if let source = Cell.reusableSource {
switch source {
case .cls:
cell = collectionContext?.dequeueReusableCell(of: Cell.self, for: self, at: index)
case .nib:
cell = collectionContext?.dequeueReusableCell(withNibName: Cell.nibName, bundle: Cell.nibBundle, for: self, at: index)
case .storyboard:
cell = collectionContext?.dequeueReusableCellFromStoryboard(withIdentifier: Cell.identifier, for: self, at: index)
}
} else {
cell = collectionContext?.dequeueReusableCell(of: Cell.self, for: self, at: index)
}
guard let cell = cell else {
fatalError("cell is nil")
}
if let cell = cell as? ListBindable, let model = model {
cell.bindViewModel(model)
}
return cell
}
override func didUpdate(to object: Any) {
model = object as? Model
}
override func didSelectItem(at index: Int) {
log("选中了")
}
}
size 有 model 遵循协议提供,cell 由复用创建, didSelectItem这里其实也要最好拋到 viewConroller 中,虽然我们可以获得viewController,但是viewController 在 sectionController 中使用意味着 sectionController 与 vc 耦合了,如果考虑复用的话最好是往下拋响应.我这里暂时没去实现具体的如何拋响应实现,这里涉及到一个点,就是设计一个单向的事件响应链,也是能展开说很多的,后面有时间会讲的.
这样子封装后,ListSectionController 也算基础封装完成,ListBindSectionController 的封装我暂时不细讲了,其实也是差不多原理,都是sectionController 自己实现数据源,然后抛出一个 block 或者 register 方法.最后简单讲讲 model 的使用.
Model 配置
class TestCollectionModel: DataModel, LayoutCachable {
var cellSize: CGSize {
MakeSize(250, 77)
}
var name: String?
/// diffIdentifier判断是否为同一个 cell
override func diffIdentifier() -> NSObjectProtocol {
"\(Self.self) + \(name ?? "")" as NSObjectProtocol
}
/// isEqual(toDiffableObject判断数据是否有更新
override func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
guard let model = object as? TestCollectionModel else { return false }
return name == model.name
}
}
很普通的一个数据配置,diffIdentifier判断数据唯一性,同一个diffIdentifier对应同一个 cell,简单来说,你可以用 userId,circleId 什么的当做唯一 id.
使用
最后看看简化后的业务代码
class TagModelViewController: UIViewController, CollectionProvider {
typealias DataType = TestCollectionModel
// MARK: - --------------------------------------infoProperty
// MARK: - --------------------------------------UIProperty
// MARK: - --------------------------------------system
override func viewDidLoad() {
super.viewDidLoad()
collectionViewController.moveTo(self)
collectionView.sectionControllerForModel = { obj in
switch obj {
case is TestCollectionModel:
return ListSingleSectionController<TestCollectionModel,TestCollectoinCell>().layoutConfig(inset: UIEdgeInsets(top: 10, left: 0, bottom: 0, right: 0), minimumLineSpacing: 10, minimumInteritemSpacing: 10)
default:
return ListSectionController()
}
}
// collectionView.register(controller: ListSingleSectionController<TestCollectionModel,TestCollectoinCell>.self, for: TestCollectionModel.self)
// collectionView.register(controller: TestCollectonSectionController.self, for: TestCollectionModel.self)
// collectionView.registReusable(TestCollectoinCell.self)
// collectionView.register(TestCollectoinCell.self, forCellWithReuseIdentifier: "TestCollectoinCell")
list = ["gg", "asdcc", "asdcc123"].map({ text in
let model = TestCollectionModel()
model.name = text
return model
})
// collectionView.performBatchUpdates(<#T##updates: (() -> Void)?##(() -> Void)?##() -> Void#>, completion: <#T##((Bool) -> Void)?##((Bool) -> Void)?##(Bool) -> Void#>)
adapt.reloadData(completion: nil)
// adapt.reloadObjects(list)
// adapt.reloadData {[weak self] _ in
// self?.adapt.performUpdates(animated: true, completion: nil)
// }
}
// MARK: - --------------------------------------actions
// MARK: - --------------------------------------net
deinit {
log("💀💀💀------------ \(Self.self)")
}
}
仅仅几行,我们就可以实现一个 collectionController,而且我们这个控制器继承于UIViewController,所以你可以自己定义你想要的基类.
UI 虽丑,但是希望大家注意的是原理.

到此,CollectionProvider 的基础
封装就结束了,还有哪些剩下的没做的工作呢:
- refresh 控件的添加
- dataSource 的抽离
后面我会单独拎出来讲讲.
学习之路漫漫其修远兮, 吾将上下而求索
网友评论