Swift

iOS(Swift) Provider一种比较优雅的封装(Tab

2021-07-04  本文已影响0人  简单coder

在之前的文章中,我在嵌套滚动文章中提到了一种以比较优雅的形式去实现 table&colleciton,今天就是来填坑的.

Provider,设计理念,就是为对象提供它本身不具有的功能,在 UI 层,我们可以思考一下,有时候的设计,我们会为了统一实现方便,为 UIViewControlller 增加基类控制器,然后把一些通用逻辑放在基类中实现,继承的思想无可厚非,并且,有时候,我们设置会为了一些通用 UI,去设计 BaseTableViewController,BaseCollectionViewController,又或者不设计封装,只是手动地添加 Table&Collection,这里就会有分歧出来,仁者见仁,智者见智.在此,我不做设计上的评判,我只是提供另外一个思路,供大家参考.

在之前的案例中, pager 的子控制器OnlineViewController是通过继承 tableviewController 的功能实现的逻辑,这样 table 的代理,数据源全部都需要我们业务 Controller 去管理,这样,就会多出很多跟业务无关的代码,占据了控制器.

class OnlineViewController: UITableViewController, UIGestureRecognizerDelegate, ScrollStateful {  }

我们可以换一个角度思考,我们可以认为 table和 collection 只是 UIViewController 的一个小组件,我们完全没必要通过继承的方式去实现它的功能,我们只需要一个组件,它自己去管理table 的周期.在这样的前提下我们可以实现.在其中,我会顺便讲解下我们 Provider 的封装细节.

原始实现

最初的模型中,没有封装的话,我们 tableview 是由控制器创建的,然后实现数据源,代理


封装

我以 table为例,定义一个 TableProvider 协议

protocol TableProvider: UIViewController {
    associatedtype DataType: DiffableJSON
    var tableViewController: TableViewController<DataType> { get }
    var tableView: TableView { get }
}

并且为其拖展属性

extension TableProvider {
    var tableViewController: TableViewController<DataType> {
        get {
            associatedObject(&tableViewControllerKey) { TableViewController<DataType>() }
        }
        set {
            setAssociatedObject(&tableViewControllerKey, newValue)
        }
    }
    
    var tableView: TableView {
        tableViewController.tableView
    }
    
    var list:[DataType] {
        get {
            tableViewController.list
        }
        set {
            tableViewController.list = newValue
        }
    }
}
这样子, Online就有了一个子控制器属性: tableViewController, 及其拖展属性,接下来就是疯狂的实现tableViewController内部的逻辑.下面是我实现 tableviewController 的代码结构,跟我在公司用的有挺多不一样的.接下里我会细致讲讲.

初始化,MultiScroll 啥的配置就没必要讲了,就是普通配置的那一套,这里着重讲一下 dataSource 和 delegate 的配置.

func numberOfSections(in tableView: UITableView) -> Int { 1 }
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        list.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let model = list[safe: indexPath.row],
              let cell = tableView.cell(for: model, indexPath: indexPath) else {
            return UITableViewCell()
        }
        
        (cell as? ListBindable)?.bindViewModel(model)
        return cell
    }

复用的 cell查找,我们不用系统的代理方法,改成自己手动配置

final func cell(for model: Any, indexPath: IndexPath) -> UITableViewCell? {
        if let cell = cellForModel?(self, model, indexPath) {// 是否手动配置 cell
            return cell
        }
        if let model = model as? NSObjectProtocol {
            if let identifier = identifier(for: model) {// 是否是注册 cell
                return dequeueReusableCell(withIdentifier: identifier, for: indexPath)
            }
        }
        return nil
    }

这里可以为大家提供一个思路,通常我们使用系统的时候,用注册 Cell.self For Cell.identify,但是我们想要以数据驱动 cell 的 UI 展示,我们可以为了 table 拖展类似 Dict, Array来增强 model 与 cell 的绑定.我这里提供了我司的绑定代码逻辑

cell复用

final func register<T: UITableViewCell, O: NSObjectProtocol>(cell: T.Type, for model: O.Type) {
        registReusable(cell)
        if let model = model as? NSObject.Type {
            registeredIdentifiers += [(model.classForCoder(), cell.identifier, cell)]
        }
    }

其实就是为 tableview增加一个model 与 cell 的元祖数组

var registeredIdentifiers: [(AnyClass, String, AnyClass)] {
        get {
            guard let identifiers = property(for: &Keys.UITableView.registeredIdentifiers) as? [(AnyClass, String, AnyClass)] else {
                let identifiers = [(AnyClass, String, AnyClass)]()
                setProperty(for: &Keys.UITableView.registeredIdentifiers, identifiers)
                return identifiers
            }
            return identifiers
        }
        set {
            setProperty(for: &Keys.UITableView.registeredIdentifiers, newValue)
        }
    }

在调用注册的时候,添加到数组里.同时顺便也使用系统的注册,把 cell和 cell.id 注册进去

// MARK: Regist reusable cell
    /// (regist Cell).
    final func registReusable<T: UITableViewCell>(_ cell: T.Type) {
        let name = String(describing: cell)
        let xibPath = Bundle.main.path(forResource: name, ofType: "nib")
        if let path = xibPath {
            let exists = FileManager.default.fileExists(atPath: path)
            if exists {
                register(cell.nib, forCellReuseIdentifier: cell.identifier)
            }
        } else {
            register(cell.self, forCellReuseIdentifier: cell.identifier)
        }
    }

这种写法,可以满足绝大部分的业务需求,使用时,仅仅需要

tableView.register(cell: OnlienCell.self, for: OnlineModel.self)

但是,总会有特殊的需求,不同的 cell,使用的是同一种 model 模型,这时,我们可以提供特殊的注册.
为 tableview 增加拖展,实现例子如下:

tableView.cellForModel = { [weak self] (tbv, model, indexPath) in
            guard let self = self,
                let message = model as? ChatMessage else {
                    return UITableViewCell()
            }
            var cell: ChatBaseCell?
            let type = message.msgType
            if message.attribute.unsupportedMessage != nil || type.contains(.notice) || type.contains(.airWave) {
                cell = tbv.reuseCell(for: indexPath, cellType: ChatNoticeCell.self)
            } else if type.contains(.friendRequest), message.sender != .loginerID {
                cell = tbv.reuseCell(for: indexPath, cellType: ChatPollCell.self)
            } else if type.contains(.text) {
                cell = tbv.reuseCell(for: indexPath, cellType: ChatTextCell.self)
          }
}

这也算是二八原则的一种提现,我们常用的永远只是那20%.

CellHeight

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        if list.count == 0, let modelType = T.self as? LayoutCachable.Type {
            // 为 skeleton做准备
            return modelType.cellHeight
        }
        guard let model = list[safe: indexPath.row] else {
            if let (_, _, cls) = tableView.registeredIdentifiers.first, let cachable = cls as? LayoutCachable.Type {
                // 从注册 cell 取静态cellHeight
                return cachable.cellHeight
            }
            return tableView.rowHeight
        }
        if let model = model as? LayoutCachable {// 从 model 取
            return model.cellHeight
        }
        if let (_, _, cls) = tableView.classAndIdentifier(for: model), let cachable = cls as? LayoutCachable.Type {// 从 model 静态取
            return cachable.cellHeight
        }
        return tableView.rowHeight
    }

我们提供一种协议 LayoutCacheable

protocol LayoutCachable {
    static var cellHeight: CGFloat { get }
    static var cellSize: CGSize { get }
    var cellHeight: CGFloat { get }
    var cellSize: CGSize { get }
}

extension LayoutCachable {
    static var cellHeight: CGFloat { 0 }
    static var cellSize: CGSize { .zero }
    var cellHeight: CGFloat { Self.cellHeight }
    var cellSize: CGSize { Self.cellSize }
}

一般由 Model 去实现这个协议,并且返回相对应的高度即可.

class OnlineModel: User, LayoutCachable {
    var cellHeight: CGFloat = 80
}

或者是 Cell 中实现,不过最好是 model 驱动

Select

func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        // 如果tablviewController 的 parent 实现了UITableViewDelegate,并且能够响应didSelectRowAt方法
        if let delegate = self.parent as? UITableViewDelegate,
           delegate.responds(to: #selector(tableView(_:didSelectRowAt:))) {
            delegate.tableView?(tableView, didSelectRowAt: indexPath)
            return
        }
        if let model = list[safe: indexPath.row] {
            selectCellInput.send(value: model)
            return
        }
    }

从 Select开始的后续代理基本都是这样,我们先考虑 parent(即业务控制器)是否实现了UITableViewDelegate,并且能够响应响应的代理方法,然后再考虑通用逻辑.不过我个人推荐,走自定义的信号或者 block,这样我们可以在内部将 model 处理完成返回,以 model 驱动,不过,总是需要提供特殊处理.
这里简单展示下代码即可

func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let delegate = self.parent as? UITableViewDelegate {
            delegate.tableView?(tableView, willDisplay: cell, forRowAt: indexPath)
        }
    }
    
    func tableView(_ tableView: UITableView, didEndDisplaying cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        if let delegate = self.parent as? UITableViewDelegate {
            delegate.tableView?(tableView, didEndDisplaying: cell, forRowAt: indexPath)
        }
    }
    
    func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
        guard let model = list[safe: indexPath.row] else { return false }
        if let delegate = self.parent as? UITableViewDataSource,
           delegate.responds(to: #selector(tableView(_:canEditRowAt:))) {
            return delegate.tableView?(tableView, canEditRowAt: indexPath) ?? false
        }
        if let can = canEditClosure?(model, indexPath) {
            return can
        }
        return false
    }
    
    func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        if let delegate = self.parent as? UITableViewDelegate,
           delegate.responds(to: #selector(tableView(_:heightForHeaderInSection:))) {
            return delegate.tableView?(tableView, heightForHeaderInSection: section) ?? .min
        }
        if let height = tableHeaderFooterProvider?(section, .header).1 {
            return height
        }
        return .min
    }
    
    func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        if let delegate = self.parent as? UITableViewDelegate,
           delegate.responds(to: #selector(tableView(_:heightForFooterInSection:))) {
            return delegate.tableView?(tableView, heightForFooterInSection: section) ?? .min
        }
        if let height = tableHeaderFooterProvider?(section, .footer).1 {
            return height
        }
        return .min
    }
    
    func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        if let delegate = self.parent as? UITableViewDelegate,
           delegate.responds(to: #selector(tableView(_:viewForHeaderInSection:))) {
            return delegate.tableView?(tableView, viewForHeaderInSection: section)
        }
        if let view = tableHeaderFooterProvider?(section, .header).0 {
            return view
        }
        return nil
    }
    
    func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        if let delegate = self.parent as? UITableViewDelegate,
           delegate.responds(to: #selector(tableView(_:viewForFooterInSection:))) {
            return delegate.tableView?(tableView, viewForFooterInSection: section)
        }
        if let view = tableHeaderFooterProvider?(section, .footer).0 {
            return view
        }
        return nil
    }

tableHeaderFooterProvider只是一个 block 的需求回调,由外部实现即可
var tableHeaderFooterProvider: ((_ section: Int, _ type: HeaderFooterType) -> (UIView?, CGFloat?))?

写框架就是这样,为了省去90%重复的内容,需要在基础框架里不断兼容考虑

ScrollViewDelegate

//MARK:- --------------------------------------ScrollViewDelegate
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewDidScroll(_:))) {
            delegate.scrollViewDidScroll?(scrollView)
        }
    }
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewDidZoom(_:))) {
            delegate.scrollViewDidZoom?(scrollView)
        }
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewWillBeginDragging(_:))) {
            delegate.scrollViewWillBeginDragging?(scrollView)
        }
    }
    
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewWillEndDragging(_:withVelocity:targetContentOffset:))) {
            delegate.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
        }
    }
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewDidEndDragging(_:willDecelerate:))) {
            delegate.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
        }
    }
    
    func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewWillBeginDecelerating(_:))) {
            delegate.scrollViewWillBeginDecelerating?(scrollView)
        }
    }
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if let delegate = self.parent as? UIScrollViewDelegate,
           delegate.responds(to: #selector(scrollViewDidEndDecelerating(_:))) {
            delegate.scrollViewDidEndDecelerating?(scrollView)
        }
    }

最后稍微拖展一下 tableviewController

    @discardableResult
    func moveTo(_ viewController: UIViewController) -> Self {
        willMove(toParent: viewController)
        viewController.view.addSubview(tableView)
        tableView.frame = viewController.view.bounds
        viewController.addChild(self)
        didMove(toParent: viewController)
        return self
    }
}

到此为之,我们算是完成了初步的封装,然后在 业务控制中使用起来就是这样的.
至此,我们可以回到业务层,去实现这样一个 table 页面

Controller

class OnlineViewController: UIViewController, TableProvider {
    typealias DataType = DataModel
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .clear
        
        tableViewController.moveTo(self)
        tableView.register(cell: OnlienCell.self, for: OnlineModel.self)
        tableViewController.selectCell.observeValues { model in
            guard let model = model as? OnlineModel else { return }
            
        }
        list = JSONUtil.deserializeArrayJsonFile(OnlineModel.self, filePath: "online")
        tableView.reloadData()
    }
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        tableView.pin.all()
    }
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        log("\(Self.self)出现了")
    }
}

OnlineModel

class OnlineModel: User, LayoutCachable {
    var cellHeight: CGFloat = 80
    
    override func diffIdentifier() -> NSObjectProtocol {
        "OnlineModel" + "\(userId)" as NSObjectProtocol
    }
    override func isEqual(toDiffableObject object: ListDiffable?) -> Bool {
        guard let obj = object as? OnlineModel else { return false }
        return nickname == obj.nickname && headUrl == obj.headUrl
    }
}

OnlineCell


import Foundation
import FlexLayout

class OnlienCell: TableViewCell {
    let avatar: AvatarView = AvatarView()
    let nameLabel = UILabel(text: "", font: .regular(17), color: .title)
    let descLabel = UILabel(text: "", font: .regular(12), color: .text)
    let badge = Badges([.genderAge, .charm, .rich, .simpleCertified, .level])
    override func commonInit() {
        super.commonInit()
        rootFlex.marginHorizontal(16).width(100%).row.alignItems(.center).justifyContent(.start).define {
            $0.addItem(avatar).size(50)
            $0.addItem().marginLeft(8).column.grow(1).shrink(1).define {
                $0.addItem().row.grow(1).shrink(1).define {
                    $0.addItem(nameLabel).shrink(1)
                    $0.addItem(badge).marginLeft(5)
                }
                $0.addItem(descLabel).marginTop(10)
            }
        }
    }
    
    override func bindViewModel(_ viewModel: Any) {
        guard let viewModel = viewModel as? OnlineModel else { return }
        
        avatar.load(viewModel.headUrl)
        
        nameLabel.text = viewModel.nickname
        descLabel.text = viewModel.mySign ?? "这个人很懒, 没留下的足迹"
        badge.update(viewModel)
        [nameLabel, descLabel, badge].flexMarkDirty()
        setNeedsLayout()
    }
}

总共写完,Controller大概也只用了20行左右.这种子类控制器,也能实现,嵌套滚动的逻辑,同时,它是继承与 UIViewController 的,没有任何中间商层级继承!.看下效果:



到此,我们 tableviewController 实现的协议如下:

class TableViewController<T: DiffableJSON>: UIViewController, ScrollStateful, UITableViewDelegate, UITableViewDataSource, UIGestureRecognizerDelegate

但是,我们一版的工作算是完成,但是又没有完全完成,我们还没有实现 Empty,Skeleton,Refresh,以及 collectionProvider+IGListKit,这些会放到后面再讲.

我给大家提供的代码是我自己实现的,借鉴了 wildog 大神的思路逻辑,我写这些文章,是想给大家提供另外一种解决思路,希望大家好好对待自己代码封装,希望在写业务时,只需要考虑业务层,不用烦心于框架层,但是,我希望每个人都能参与到架构的封装,至少,我希望每个人都能维护好公司的项目框架

总结

去年整年,我都忙于业务逻辑,框架层我基本都没有参与(其实也没太多必要参与,wildog大神 给我们留下了太多的瑰宝)我希望每个人都能有自己的思考,包括了业务上的,也包括框架层的.比如 tableprovider,公司的代码将 datasource 层又抽了一次,以便于 tabledatasource 和 collectionDatasource 的管理,但是我这里没有做这一层的封装,因为这个封装是我一点点写下来的,dataSource 的封装需要的代码量更多,不知道大家有没有注意到在 dataSource 那里, 有这么一行func numberOfSections(in tableView: UITableView) -> Int { 1 }这说明了什么,我目前的框架结构完全忽略了分组 table 的业务实现,硬实现也能实现,但是大家想一下,我 list:[[]]结构和[]结构,在写框架过程中,总是去考虑[[]]的可能性,那代码看上去会很别扭. 那么如果我们讲 datasource 抽出来,抽出一个 ListDataSource 和一个 SectionDataSource,将其中的内容分别放入,这样逻辑不就非常的清晰了吗,我估计当时这肯定也是其中的一个原因,促成了 wildog 当初分离出 DataSource 层.

后续有时间,我还是会抽出这层 dataSource 层,毕竟,框架框架,必须要兼容所有需要的逻辑,才是一个好框架.
如果后面还有时间,我想总结一下我司的礼物层封装,它也是一个比较大的逻辑实现,包括 pointInside, hittest 控制点击,捕获点击,windowlevel 控制礼物 window,OperationQueue,礼物队列排列及使用信号量控制最大异步并发数,以及跑马灯等等,算是有非常多的逻辑了,不过,得等 provider 完全完成后再去实现(Flag),毕竟礼物队列算是锦上添花,table,collection 是项目最基础的必要封装.

上一篇下一篇

猜你喜欢

热点阅读