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