美文网首页Swift
iOS(Swift) Provider续-CollectionP

iOS(Swift) Provider续-CollectionP

作者: 简单coder | 来源:发表于2021-08-05 00:07 被阅读0次

本篇为续篇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 的抽离
    后面我会单独拎出来讲讲.

学习之路漫漫其修远兮, 吾将上下而求索

相关文章

网友评论

    本文标题:iOS(Swift) Provider续-CollectionP

    本文链接:https://www.haomeiwen.com/subject/zausvltx.html