iOS 小组件 - 标签瀑布流Base组件抽取,APP业务重构(一)

2025.02.04 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。
在上进青年 - 三大社团(2023.07-2023.08)新版本开发中,再一次遇到了需要做不同样式的标签瀑布流而祖传代码,难以复用、维护。

借此版本验收期间,对标签瀑布流Base组件 进行了抽离,并且重构了所有相关业务,为将来不同样式的标签瀑布流提供了更简便快捷的开发体验

标签瀑布流业务样式示例

一、祖传的标签代码

原来有极多相似的祖传代码,在业务需求开发完成后,已经有四份相似的拷贝代码了,考虑到以后可能越来越多相似业务,决定着手重构。

二、标签瀑布流Base抽取(组件源码)

1. BaseTag组件结构
2. YAYBaseTagView 瀑布流BaseView

主视图,主要负责加载配置,组装布局,其中主要的代码:

swift 复制代码
//  标签瀑布流 BaseView

import UIKit

class YAYBaseTagView: UICollectionView, UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout {
    
    /// 数据源
    var dataArray: [YAYBaseTagModel] = []
    
    /// 标签配置
    var option: YAYBaseTagOption
    /// 标签FlowLayout
    let layout = YAYBaseTagFlowLayout()
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
        
    init(frame: CGRect, option: YAYBaseTagOption) {
        self.option = option
        layout.minimumLineSpacing = option.minimumLineSpacing
        layout.minimumInteritemSpacing = option.minimumInteritemSpacing
        layout.sectionInset = UIEdgeInsets(top: option.topM, left: option.leftM, bottom: option.bottomM, right: option.rightM)
        super.init(frame: frame, collectionViewLayout: layout)
        clipsToBounds = true
        showsHorizontalScrollIndicator = false
        showsVerticalScrollIndicator = false
        dataSource = self
        delegate = self
        backgroundColor = .clear
        // 注册cell方法
        register(cellClassFromString(option.cellClass).self, forCellWithReuseIdentifier: option.cellClass)
    }
    
    /// 字符串转类type
    func cellClassFromString(_ className:String) -> AnyClass {
        // 1、获swift中的命名空间名
        var name = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String
        // 2、如果包名中有'-'横线这样的字符,在拿到包名后,还需要把包名的'-'转换成'_'下横线
        name = name?.replacingOccurrences(of: "-", with: "_")
        // 3、拼接命名空间和类名,"包名.类名"
        let fullClassName = name! + "." + className
        // 4、因为NSClassFromString()返回的AnyClass?,需要给个默认值返回!
        let classType: AnyClass = NSClassFromString(fullClassName) ?? YAYBaseTagCell.self
        // 类type
        return classType
    }

    
    // MARK: - 可重写的方法
    
    // 加载标签数组
    func reload(array: [YAYBaseTagModel]) {
        self.dataArray = array
        reloadData()
    }
    
    /// 获取item大小
    func getSizeForItem(_ tagModel: YAYBaseTagModel, collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return CGSize.zero
    }
}



// MARK: - collectionView Delegate
extension YAYBaseTagView {
    
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        dataArray.count
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: option.cellClass, for: indexPath) as? YAYBaseTagCell ?? YAYBaseTagCell()
        cell.tagOptionSet(option: option)
        cell.bindData(model: dataArray[indexPath.row])
        return cell
    }
  
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        return getSizeForItem(dataArray[indexPath.row], collectionView: collectionView, layout: layout, sizeForItemAt: indexPath)
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        
    }
}

getSizeForItem() 每个业务都需要继承重写的方法,定义每个标签元素(item) 的大小计算方式。

3. YAYBaseTagCell 瀑布流BaseCell

每一个标签元素(item)视图,其中可以继承重写的主要代码有 样式维护 tagOptionSet()数据绑定 bindData()

swift 复制代码
//  标签BaseCell

import UIKit


class YAYBaseTagCell: UICollectionViewCell {
    
    var option = YAYBaseTagOption()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .clear
        
        contentView.addSubview(rightBackView)
        contentView.addSubview(titleLabel)
        contentView.addSubview(subTitleLabel)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - 可重写的方法
    
    // 改变样式
    func tagOptionSet(option: YAYBaseTagOption) {
        contentView.backgroundColor = option.itemBackGroundColor
        contentView.layer.cornerRadius = option.itemCorner
        
        titleLabel.textColor = option.itemTextColor
        subTitleLabel.textColor = option.itemTextColor
        titleLabel.textColor = option.itemTextColor
        subTitleLabel.textColor = option.itemTextColor
        titleLabel.font = option.titleFont ?? UIFont.systemFont(ofSize: option.fontSize, weight: option.fontWeight)
        subTitleLabel.font = option.titleFont ?? UIFont.systemFont(ofSize: option.fontSize, weight: option.fontWeight)
    }
    
    /// 绑定数据
    func bindData(model: YAYBaseTagModel) {
        
    }
    
    // MARK: - lazy
    lazy var titleLabel: UILabel = {
        let titleLabel = UILabel()
        titleLabel.textAlignment = .center
        return titleLabel
    }()
    
    lazy var subTitleLabel: UILabel = {
        let subTitleLabel = UILabel()
        subTitleLabel.textAlignment = .center
        return subTitleLabel
    }()
    
    lazy var rightBackView: UIImageView = {
        let rightBackView = UIImageView(image: UIImage(named: "rightBack_Template"))
        rightBackView.isUserInteractionEnabled = true
        rightBackView.tintColor = .white
        rightBackView.isHidden = true
        return rightBackView
    }()
}
4. YAYBaseTagFlowLayout 瀑布流BaseFlowLayout

自定义流式布局计算类,重写了系统级方法:

swift 复制代码
//  标签瀑布流 BaseFlowLayout

import UIKit

class YAYBaseTagFlowLayout: UICollectionViewFlowLayout {
    
    var caculateTotalHeight: ((CGFloat) -> Void)?

    override func layoutAttributesForElements(in rect: CGRect) -> [UICollectionViewLayoutAttributes]? {
        guard let originalAttributes = super.layoutAttributesForElements(in: rect) else {
            return nil
        }
        var leftMargin: CGFloat = sectionInset.left
        
        var maxY: CGFloat = -1.0
        
        let attributes = originalAttributes.map { (attribute) -> UICollectionViewLayoutAttributes in
            
            if attribute.frame.maxY > maxY {
                leftMargin = sectionInset.left
                maxY = attribute.frame.maxY
            }
            attribute.frame.origin.x = leftMargin
            leftMargin += attribute.frame.width + minimumInteritemSpacing
            
            return attribute
        }
        caculateTotalHeight?(attributes.last?.frame.maxY ?? 0.0)
        return attributes
    }
}
5. YAYBaseTagModel 瀑布流BaseTagModel

数据模型层,主要属性有:

swift 复制代码
//  标签瀑布流 BaseTagModel

import Foundation

class YAYBaseTagModel: NSObject, HandyJSON {
    
    @objc var labelId: Int = 0
    @objc var labelName: String = ""
    @objc var labelTimes: Int = 0
    var isSelected: Bool?
    
    /// 显示文本
    var context: String = ""
    /// 展开/收起图片
    var moreImgName: String = ""
    
    /// 辅助属性(存储数据源 - 回调用)
    var modelValue: Any?
    
    override required init() {
        super.init()
    }
    
    init(labelId: Int, labelName: String, labelTimes: Int) {
        super.init()
        self.labelId = labelId
        self.labelName = labelName
        self.labelTimes = labelTimes
    }
}
6. YAYBaseTagOption 瀑布流base配置类

标签的各种自定义常用配置,主要属性有:

swift 复制代码
//  标签瀑布 base配置类

import UIKit

class YAYBaseTagOption: NSObject {
    
    /// 类名 (要加载的cell)
    var cellClass: String = "YAYBaseTagCell"
    
    /// collectionView 左间距
    var leftM: CGFloat = 0
    /// collectionView 右间距
    var rightM: CGFloat = 0
    /// collectionView 一行标签总宽度(contentWidth = frameWidth - leftM - rightM)
    var contentWidth: CGFloat = 0
    /// collectionView 上间距
    var topM: CGFloat = 0
    /// collectionView 下间距
    var bottomM: CGFloat = 0
    
    /// 标题高度
    var titleHeight: CGFloat = 0
    /// 跟随滑动方向的行间距
    var minimumLineSpacing: CGFloat = 0
    /// 跟随滑动方向的每个item间距
    var minimumInteritemSpacing: CGFloat = 0
    /// item背景颜色
    var itemBackGroundColor: UIColor = UIColor.white
    /// 标题颜色
    var itemTextColor: UIColor = .white
    /// item高度
    var itemHeight: CGFloat = 0
    /// item圆角
    var itemCorner: CGFloat = 0
    /// 字体大小
    var fontSize: CGFloat = 0
    /// 字体
    var fontWeight: UIFont.Weight = .regular
    /// 自定义字体(弃用,因为计算文本宽度需要拿到UIFont.Weigh,用自定义titleFont拿不到weight)
    var titleFont: UIFont?
    /// 标题和item左边间距
    var itemLeftMargin: CGFloat = 0
    /// 标题和item右边间距
    var itemRightMargin: CGFloat = 0
    

    override init() {
        super.init()
    }
}

三、重构后,业务使用示范(个人点赞标签)

YAYMyLikeTagView 业务瀑布流主要视图:
swift 复制代码
//  上进赞标签瀑布
class YAYMyLikeTagView: YAYBaseTagView {
    
    // 当前用户的orgId 和 userId
    var orgId: Int = 0
    var orgUserId: Int = 0
    
    /// 重写计算标签文本方法
    override func getSizeForItem(_ tagModel: YAYBaseTagModel, collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let tagModel = dataArray[indexPath.row]
        let text = "\(tagModel.labelName) \(tagModel.labelTimes)"
        let width: CGFloat = text.textWidth(fontSize: option.fontSize, height: option.titleHeight) + option.itemLeftMargin + option.itemRightMargin
        // 超过限制,文本宽度要恢复才能出现...
        return CGSize(width: min(option.contentWidth - 5, width), height: option.itemHeight)
    }
    
    // 重写base,自定义点击事件
    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        super.collectionView(collectionView, didSelectItemAt: indexPath)
        
        if dataArray[indexPath.row].labelName.hasPrefix("更多") {
            // 路由跳转
        }
        else {
            // 路由跳转
        }
    }
}
YAYMyLikeTagCell 业务标签cell,主要是重写绑定网络请求数据,具体代码,略。
YAYMyLikeTagViewOption 业务瀑布流配置
swift 复制代码
    class YAYMyLikeTagViewOption: YAYBaseTagOption {
        
        init(leftM: CGFloat = 15, rightM: CGFloat = 15) {
            super.init()
            
            self.leftM = leftM
            self.rightM = rightM
            contentWidth = screenWidth - leftM - rightM
            cellClass = "YAYMyLikeTagCell"
            titleHeight = 17
            itemHeight = 24
            itemCorner = 12
            minimumLineSpacing = 8
            minimumInteritemSpacing = 15
            fontSize = 12
            itemLeftMargin = 10
            itemRightMargin = 12
            itemBackGroundColor = UIColor.hexColor(hex: "#FFFFFF", alpha: 0.3)
            itemTextColor = .white
        }
    }

最终运行效果:

四、重构后,新业务开发

三大社团新版本,新的分享页面需要展示的效果:

我们可以看到,与上面的旧业务瀑布流相比,文本内容是一致的,只有UI元素不一致(圆角、颜色、字体大小),这时候开发新的业务就简单多了。

没错!!! 这时候只需要新建一个继承于Base文件 YAYBaseTagOption 的业务配置文件就可以了。比如叫 YAYLikeListTagPosterViewOption

swift 复制代码
    /// 三大社团版本 - 点赞海报标签option
    class YAYLikeListTagPosterViewOption: YAYBaseTagOption {
        
        init(leftM: CGFloat = 16, rightM: CGFloat = 16) {
            super.init()
            
            self.leftM = leftM
            self.rightM = rightM
            contentWidth = screenWidth - leftM - rightM
            cellClass = "YAYMyLikeTagCell"
            titleHeight = 16
            itemHeight = 24
            itemCorner = 4
            minimumLineSpacing = 10
            minimumInteritemSpacing = 10
            itemLeftMargin = 6
            itemRightMargin = 6
            itemBackGroundColor = UIColor.hexColor(hex: "#FFF2DA")
            itemTextColor = .hexColor(hex: "#985609")
            fontSize = 10
        }
    }

应用了这个配置类后,直接引用旧业务的 YAYMyLikeTagView ,传入配置类即可,实现不同样式相同功能的瀑布流的时候只需要5分钟就可以完成功能开发。

五、总结

面对有可能重复使用的模块,越早重构,未来开发效率越高

只要做好Base组件的设计,未来开发新业务的就会体现出便利性与优雅,而不需要笨拙地重新拷贝一份代码,拜读一遍后再修改

从8.9号,开始着手整理文件结构,开始初步搭建Base,共花了2天,至8.14号,完成原有4个业务的业务抽离与继承重构,共花了5天。

所以,遇到屎山代码一定要尽早重构 !!!不然维护和开发成本只会越来越大。

最最最后,完结撒花

相关推荐
Unlimitedz16 小时前
封装常用控制器
ios·swift
一丝晨光18 小时前
为什么会有函数调用参数带标签的写法?Swift函数调用的参数传递需要加前缀是否是冗余?函数调用?函数参数?
java·开发语言·c++·ios·c#·objective-c·swift
DZh_Ming1 天前
IOS开发日志-ios新建项目后-将storyboard去掉,版本调整为IOS13以下
macos·ios·cocoa
小鹿撞出了脑震荡1 天前
Effective Objective-C 2.0 读书笔记—— 接口与API设计
开发语言·ios·objective-c
小鹿撞出了脑震荡1 天前
Effective Objective-C 2.0 读书笔记——类对象
开发语言·ios·objective-c
職場上的造物主3 天前
高清种子资源获取指南 | ✈️@seedlinkbot
python·ios·php·音视频·视频编解码·视频
Kevin Coding3 天前
Flutter使用Flavor实现切换环境和多渠道打包
android·flutter·ios
wn5315 天前
【浏览器 - Mac实时调试iOS手机浏览器页面】
前端·macos·ios·智能手机·浏览器
小鹿撞出了脑震荡6 天前
Effective Objective-C 2.0 读书笔记—— 方法调配(method swizzling)
ios·objective-c·swift