美文网首页Swift
iOS(Swift) Provider一种比较优雅的封装(Tab

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

作者: 简单coder | 来源:发表于2021-07-04 11:44 被阅读0次

    在之前的文章中,我在嵌套滚动文章中提到了一种以比较优雅的形式去实现 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 是项目最基础的必要封装.

    相关文章

      网友评论

        本文标题:iOS(Swift) Provider一种比较优雅的封装(Tab

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