iOS 基础系统组件UICollectionViewController框架搭建设计与实现探究

537 阅读6分钟

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 进行继承即可。

最最最后,完结撒花

告辞.jpeg