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

2025.02.04 工作变动原因,故将一些工作期间Tapd内部写的Wiki文档转移到个人博客。

在上进青年 - 三大社团(2023.07-2023.08)新版本开发中,再一次遇到了需要做不同样式的标签瀑布流而祖传代码,难以复用、维护。

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

标签瀑布流业务样式示例

tapd_44062861_1692172950_567.jpg

一、祖传的标签代码

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

tapd_44062861_1692173792_958.png

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

1. BaseTag组件结构

tapd_44062861_1692170981_477.png

2. YAYBaseTagView 瀑布流BaseView

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

//  标签瀑布流 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()

//  标签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

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

//  标签瀑布流 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

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

//  标签瀑布流 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配置类

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

//  标签瀑布 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 业务瀑布流主要视图:

//  上进赞标签瀑布
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 业务瀑布流配置

    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
        }
    }

最终运行效果:


tapd_44062861_1692172884_635.jpg

四、重构后,新业务开发

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


tapd_44062861_1692172950_567.jpg

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

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

tapd_44062861_1692173100_103.png

    /// 三大社团版本 - 点赞海报标签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天。


tapd_44062861_1692173877_131.png

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

tapd_44062861_1701246650_612.png

最最最后,完结撒花

告辞.jpeg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,695评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,569评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,130评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,648评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,655评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,268评论 1 309
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,835评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,740评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,286评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,375评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,505评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,185评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,873评论 3 333
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,357评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,466评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,921评论 3 376
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,515评论 2 359

推荐阅读更多精彩内容