2025.01.21 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。
鉴于工程里还没有封装base collection类,在设计可行性方案并且进行实现探究后,一次iOS组内分享会中分享自己的想法后,在这里把设计思路和源码放上来以作记录。
因为是一边学习Swift一边写的,有一些语法问题上还可以再做一步优化(比如,把一些逻辑! 强制解析
as!类型改成可选型as?防止崩溃,不过使用as!也能更好帮助开发阶段调试定位问题),先在这里做大体框架的源码设计与实现分享。
这份一是基于 《阅读类APP》 的demo
一、设计思想与实际作用
1.统一代码开发规范,命名方式
首先,因为每个程序员思考方式不同,想法不同,写出来的代码、文件命名等等,自然会有不一样的地方。如果有一套统一的、能覆盖大多数场景的开发模板(base组件),将会大大减少新员工或者相同开发人员交替开发的阅读成本、开发成本。
2.提高开发效率
有一套设计良好的基础组件模板,可以减少开发中一些基础 UICollectionViewController 功能的重复代码编写,也可以迅速的复用相似的UI层到相同的新页面中。
3.使代码更加内聚
如果能够把 UICollectionViewController 常用的功能封装后, 一个构造函数完成以前需要好几个代理方法才能写完的功能,这样一个构造函数,对于开发、维护以及更新页面的时候就会变得特别方便。
既能把处理逻辑放到一起,也能让代码顺序对应页面元素顺序,大大提高了代码的 可阅读性、可维护性、开发效率。
二、框架搭建与源码实现
base CollectionViewController 组件
YYYBaseCollectionViewController
// Created by Yimmm on 2022/6/2.
// Copyright © 2022 xj. All rights reserved.
// baseCollectionViewController组件
import UIKit
//private let reuseIdentifier = "Cell"
@objc protocol YYYCollectionViewDelegate: AnyObject {
/**
didSelect回调
collectionViewModel: 数据源collectionViewModel
didSelectCellModel: 点击Cell的CellModel
*/
@objc optional func collectionViewModel(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath)
/**
点击内CollectionViewCell里按钮的回调
cellModel: 点击Cell的CellModel
*/
@objc optional func clickCollectionViewCellInsideButton(_ collectionViewModel: YYYBaseCollectionViewModel, cellModel: YYYBaseCollectionViewCellModel, senderTag: Int)
}
class YYYBaseCollectionViewController: UICollectionViewController, YYYCollectionViewCellDelegate {
weak var delegate: YYYCollectionViewDelegate?
/// 数据源viewModel
var viewModel: YYYBaseCollectionViewModel? {
didSet {
// FIXME: - 业 注册重用(先测试这样做行不行,不行的话就和Collectionable一样,提供一个注册方法)
// 注册cell
for cellModel in viewModel!.cellModels {
collectionView.register(cellClassFromString(cellModel.cellClass).self, forCellWithReuseIdentifier: "\(cellModel.cellClass!)")
}
collectionView.reloadData()
}
}
override func viewDidLoad() {
super.viewDidLoad()
setupCollectionView()
}
func setupCollectionView() {
collectionView.dataSource = self
collectionView.delegate = self
collectionView.backgroundColor = .background_light
collectionView.showsHorizontalScrollIndicator = false
collectionView.showsVerticalScrollIndicator = false
}
// MARK: - Private Method
/// 字符串转类
func cellClassFromString(_ className:String, indexPath: IndexPath = IndexPath(item: 0, section: 0)) -> AnyClass {
// 1、获swift中的命名空间名
var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
// 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
name = name?.replacingOccurrences(of: "-", with: "_")
// 3、拼接命名空间和类名,”包名.类名“
let fullClassName = name! + "." + className
// 4、如果取不到,给个默认值
let classType: AnyClass = NSClassFromString(fullClassName) ?? YYYCollectionViewCell.self
// 本类type
return classType
}
// MARK: - UICollectionViewDataSource
override func numberOfSections(in collectionView: UICollectionView) -> Int {
// #warning Incomplete implementation, return the number of sections
return 1
}
override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let items = viewModel?.cellModels.count ?? 0
return items
}
override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let str = viewModel!.cellModels[indexPath.row].cellClass!
// 得到AnyClass
let anyClass: AnyClass = cellClassFromString(str, indexPath: indexPath)
// 强转自己需要的类
let classType = anyClass as! YYYCollectionViewCell.Type
// 使用类方法 .cell来得到一个实例对象
let cell = classType.cell(collectionView, indexPath: indexPath)
cell.cellModel = viewModel!.cellModels[indexPath.row]
cell.cellDelagte = self
return cell
}
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
self.delegate?.collectionViewModel?(viewModel!, didSelectCellModel: viewModel!.cellModels[indexPath.row], didSelectItemAt: indexPath)
}
// MARK: - YYYCollectionViewCell Delegate
func clickInsideButton(_ cellModel: YYYBaseCollectionViewCellModel, senderTag: Int) {
self.delegate?.clickCollectionViewCellInsideButton?(viewModel!, cellModel: cellModel, senderTag: senderTag)
}
}
base CollectionViewModel 组件
YYYBaseCollectionViewModel
// baseCollectionViewModel组件
import UIKit
class YYYBaseCollectionViewModel: NSObject {
/// 构建函数参数闭包
public typealias cellModelsClosure = (inout [YYYBaseCollectionViewCellModel]) -> Void
/// 数据源
var cellModels = [YYYBaseCollectionViewCellModel]()
init(cellModelsClosure: cellModelsClosure) {
cellModelsClosure(&cellModels)
}
}
class YYYBaseCollectionViewCellModel: NSObject {
/// Cell类名
var cellClass: String!
/// 标题
var fieldTitle: String!
/// 副标题
var fieldSubTitle: String!
/// 图片名
var imageName: String!
/// 图片URL
var imageURL: String!
/// 辅助字段
var others: [String]!
/// 是否只可读
var isReadOnly: Bool!
/// 辅助属性(存储数据源 - 回调用)
var modelValue: AnyObject!
override init() {
super.init()
}
}
// MARK: - 可扩展的 CollectionCellModel
class YYYBookCoverCollectionCellModel : YYYBaseCollectionViewCellModel{
/// 封面图片高度
var imageHeight: CGFloat!
}
base CollectionViewCell 组件
YYYBaseCollectionViewCell
// baseCollectionViewCell组件
import UIKit
@objc protocol YYYCollectionViewCellDelegate: AnyObject {
/**
点击内CollectionViewCell里按钮的回调
cellModel: 点击Cell的CellModel
*/
@objc optional func clickInsideButton(_ cellModel: YYYBaseCollectionViewCellModel, senderTag: Int)
}
class YYYCollectionViewCell: UICollectionViewCell {
/// 数据源cellModel
var cellModel: YYYBaseCollectionViewCellModel! {
didSet {
setupUI()
}
}
weak var cellDelagte: YYYCollectionViewCellDelegate?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
// MARK: - setup
func setupUI() {
// override
}
class func cell(_ collectionView: UICollectionView, indexPath: IndexPath) -> YYYCollectionViewCell{
return collectionView.dequeueReusableCell(withReuseIdentifier: "\(self)", for: indexPath) as! YYYCollectionViewCell
}
// MARK: - Private Method
/// 创建cell内 UIButton 通用方法
func newButton() -> UIButton {
let button = UIButton(type: .custom)
button.addTarget(self, action: #selector(clickNewButton), for: .touchUpInside)
addSubview(button)
return button
}
@objc func clickNewButton(sender: UIButton) {
self.cellDelagte?.clickInsideButton?(cellModel, senderTag: sender.tag)
}
}
三、具体使用
1.引用collectionViewController,collection具体参数由业务决定:
lazy var collectionViewController: YYYBaseCollectionViewController
2.构建collectionViewModel,并传值给collectionViewController.viewModel:
// cellModel构建方法
override func setCollectionCellModel() {
// 数据模型
let bookList = cellModel.collectionViewModel as? [HomeTop] ?? [HomeTop]()
// VM构建方法
let viewModel = YYYBaseCollectionViewModel { cellModels in
// 遍历数据模型,构建cellModel
for book in bookList {
let cellModel = YYYBookCoverCollectionCellModel()
cellModel.cellClass = "YYYBookCoverStyle1CollectionViewCell"
cellModel.fieldTitle = book.bookName
cellModel.imageURL = book.imageUrl
cellModel.imageHeight = scrW(122)
// AnyObject属性,用来回调判断等
cellModel.modelValue = book
// 构建参数cellModels列表拼接model
cellModels.append(cellModel)
}
}
// 赋值viewModel,自动注册cell,刷新列表
collectionViewController.viewModel = viewModel
}
3.实现业务需要的一些通用delegate
// MARK: - Collection Delegate
// 这是collectionViewController的 点击回调
func collectionViewModel(_ collectionViewModel: YYYBaseCollectionViewModel, didSelectCellModel cellModel: YYYBaseCollectionViewCellModel, didSelectItemAt indexPath: IndexPath) {
// 实现一下table嵌套collection的通用点击代理
self.cellDelagte?.clickInsideCollectionViewCell?(collectionViewModel, didSelectCellModel: cellModel, didSelectItemAt: indexPath)
}
四、优点和缺点
优点一、代码规范
规范常用代码写法,大部份业务都是相似的代码结构,接手后维护成本更低,也能避免不同水平程序员写出来不同风格的代码,可阅读性更好。
优点二、内聚性
使构建常见的collection时,代码更加内聚,构建新代码时间成本更低,维护旧代码可一目了然。
优点三、统一cellModel属性,业务model不会渗透通用cell和controller代码,cellModel在回调中拿来即用
构建常见的table时,后台无论怎么变化返回的数据结构,共用collectionCell都是用同一个通用的cellModel属性,不会造成引入业务model而渗透cell和controller回调的代码。同时写新UI样式时更方便,属性更统一,开发相似样式cell的时候也更容易做出更改。
缺点一、增加学习成本
熟悉框架(引用,构建ViewModel)时,因为和平常大家熟知的(继承,实现代理)写代码方式不一样,所以会有一定的学习成本。
缺点二、应对动态约束布局的UI需要自己手动刷新
比如无法用 estimatedRowHeight 与 约束布局 实现动态布局列表,框架多用于快速构建实现常见的静态布局。当然,写复杂场景时就不需要引用 lazy var collectionViewController: YYYBaseCollectionViewController 了。如果APP需要初始化一些通用配置, 搭建一个BaseViewController 进行继承即可。