iOS 组件复用实战:首页和结果页各写一套卡片,为什么很快就会失控

5 阅读4分钟

前言

做业务型 App 时,一个很常见的问题是:

  • 首页有一套风险卡
  • 结果页又有一套风险卡
  • 首页有一套指标卡
  • 结果页又有一套指标卡

短期看没什么问题,长期一定会出事。

因为只要设计稿改一次,你就得:

  • 首页改一遍
  • 结果页再改一遍
  • 最后两边还不一定完全一致

所以这次我做了一个非常明确的决定:

只要视觉一致、数据兼容,就直接跨页面复用同一套卡片组件。

什么时候应该复用,什么时候不该硬复用

我现在判断一个 UI 要不要跨页面复用,主要看三点:

  1. 视觉是否真的一致
  2. 数据结构是否兼容
  3. 复用后是否会让边界更清晰,而不是更混乱

比如:

  • 首页和结果页的风险卡视觉一样
  • 首页和结果页的健康指标卡视觉一样
  • 数据都能归一成统一结构

那就应该复用。

但像结果页顶部导航、总分卡这种页面专属结构,就不该为了“复用率”强行共用。

先把数据模型统一成展示层 DTO

复用的前提不是 view 写得多漂亮,而是数据先收敛。

比如风险卡完全可以统一成一个 DTO:

enum DisplayTone {
    case positive
    case warning
    case caution

    var valueColor: UIColor {
        switch self {
        case .positive: return UIColor(hex: "#20D292")
        case .warning: return UIColor(hex: "#FF6A4A")
        case .caution: return UIColor(hex: "#FFB020")
        }
    }

    var badgeTextColor: UIColor { valueColor }
    var badgeBackgroundColor: UIColor { valueColor.withAlphaComponent(0.08) }
}

struct RiskViewData {
    let title: String
    let valueText: String
    let statusText: String
    let descriptionText: String
    let tone: DisplayTone
}

健康指标卡也一样:

struct MetricViewData {
    let icon: UIImage?
    let title: String
    let valueText: String
    let unitText: String
    let statusText: String
    let tone: DisplayTone
}

一旦展示层数据统一了,view 的复用就会简单很多。

风险卡组件怎么抽

风险卡通常包含这些稳定结构:

  • 标题
  • 数值
  • 状态标签
  • 解释文本

这种结构非常适合抽成统一组件:

final class RiskCardView: UIView {
    private let titleLabel = UILabel()
    private let valueLabel = UILabel()
    private let statusLabel = PaddingLabel()
    private let descriptionLabel = UILabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .white
        layer.cornerRadius = 16
        layer.borderWidth = 1
        layer.borderColor = UIColor(hex: "#F0EFF8").cgColor

        [titleLabel, valueLabel, statusLabel, descriptionLabel].forEach(addSubview)

        titleLabel.font = .systemFont(ofSize: 16, weight: .bold)
        valueLabel.font = .systemFont(ofSize: 28, weight: .bold)
        descriptionLabel.numberOfLines = 2
        descriptionLabel.textColor = UIColor(hex: "#82828C")
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(with viewData: RiskViewData) {
        titleLabel.text = viewData.title
        valueLabel.text = viewData.valueText
        statusLabel.text = viewData.statusText
        descriptionLabel.text = viewData.descriptionText
        statusLabel.textColor = viewData.tone.badgeTextColor
        statusLabel.backgroundColor = viewData.tone.badgeBackgroundColor
        valueLabel.textColor = viewData.tone.valueColor
    }
}

这样首页和结果页只需要负责把各自的数据转成 RiskViewData,view 本身根本不需要知道“自己来自哪个页面”。

指标卡组件也一样

健康指标卡的稳定结构通常是:

  • 小图标
  • 标题
  • 数值
  • 单位
  • 状态标签

示例:

final class MetricCardView: UIView {
    private let iconView = UIImageView()
    private let titleLabel = UILabel()
    private let valueLabel = UILabel()
    private let unitLabel = UILabel()
    private let statusLabel = PaddingLabel()

    override init(frame: CGRect) {
        super.init(frame: frame)
        backgroundColor = .white
        layer.cornerRadius = 16
        layer.borderWidth = 1
        layer.borderColor = UIColor(hex: "#F0EFF8").cgColor

        [iconView, titleLabel, valueLabel, unitLabel, statusLabel].forEach(addSubview)

        titleLabel.numberOfLines = 2
        titleLabel.font = .systemFont(ofSize: 12, weight: .bold)
        valueLabel.font = .systemFont(ofSize: 24, weight: .bold)
        unitLabel.textColor = UIColor(hex: "#8C8B97")
        statusLabel.layer.cornerRadius = 10
        statusLabel.layer.masksToBounds = true
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func configure(with viewData: MetricViewData) {
        iconView.image = viewData.icon
        titleLabel.text = viewData.title
        valueLabel.text = viewData.valueText
        unitLabel.text = viewData.unitText
        statusLabel.text = viewData.statusText
        statusLabel.textColor = viewData.tone.badgeTextColor
        statusLabel.backgroundColor = viewData.tone.badgeBackgroundColor
    }
}

页面容器不同,卡片组件相同

这是我这次最核心的处理方式。

也就是说:

  • 首页容器负责首页结构
  • 结果页容器负责结果页结构
  • 但风险卡和指标卡本身,两边直接共用

这样做最大的收益有两个:

第一,视觉统一。
第二,后续改样式只改一处。

如果以后 badge 样式改了、圆角改了、文案区间距改了,你不会再面临“首页改完,结果页忘了改”的问题。

复用时一个很重要的边界:别把页面专属结构也硬塞进去

虽然风险卡和指标卡很适合复用,但不是所有结果页元素都该复用。

比如下面这些更适合单独做:

  • 结果页顶部导航
  • PDF 按钮
  • 总分环形卡
  • 首页 Hero 区
  • 首页进度卡

原因很简单:
这些不是“通用卡片”,而是“页面专属结构”。

如果为了复用而把这些东西都塞进一个超级组件,最后只会让代码越来越难维护。

总结

真正有价值的组件复用,不是为了少写几个 view,而是为了让:

  • 同一类 UI 在多个页面里长期保持一致
  • 后续设计改动只改一处
  • 页面结构和卡片结构边界更清晰

如果你也在做类似的首页/结果页双页面改造,我建议你先问自己三个问题:

  1. 视觉是否真的一致?
  2. 数据结构是否能统一?
  3. 复用后边界是否更清晰?

如果答案都是“是”,那就果断复用。

一句话总结:

组件复用真正的价值,不是省代码,而是长期降低样式分叉和维护成本。