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天。
所以,遇到屎山代码一定要尽早重构 !!!不然维护和开发成本只会越来越大。