游乐游手机版
首页/AI教程/文章详情

iOS首页进度卡开发:状态边界比渐变条更难

时间:2026-06-26 16:28
首页进度卡应作为有明确状态边界的组件,建立显式状态模型区分追踪与完成,确保UI数据一致。组件仅抛事件,业务逻辑由页面控制器处理。渐变进度条用CAGradientLayer实现动态宽度,全屏弹窗挂载window避免遮挡。核心是维护状态、交互与显示边界,保障长期可维护性。

前言

在健康、训练与打卡类应用中,首页常展示一种“连续 N 天完成”的状态卡片。表面上看只是一个普通模块,但实际它集成了进度展示、剩余天数提示、完成状态切换、二次操作入口及自定义弹窗等多重功能。若仅临时叠加多个控件,后期维护将变得极其困难。

一个更清晰的思路是:将其设计为一个双状态组件,而非一张“会变色的卡片”。

iOS 首页进度卡实战:最难的不是渐变进度条,而是状态边界

为何进度卡不能仅靠 isHidden 打补丁

许多开发者在实现进度卡时倾向于:先堆砌所有控件,在tracking状态隐藏部分元素,completed状态再显示另一部分。短期内看似简便,长期却导致两大典型问题:状态逻辑难以维护,交互事件相互干扰。

更推荐的做法是显式建立一个状态枚举模型:

enum ProgressCardStyle {case tracking(remainingDays: Int, completedDays: Int)case completed}

这样,组件在 configure 时无需猜测显示内容,状态清晰明了。

组件层仅处理状态和事件,不涉及业务逻辑

本次设计的进度卡最终暴露了三个清晰的事件:onInfoTaponRecalculateTaponUnlockTap。组件只负责传递用户行为,至于点击后弹出内容、是否重置、如何跳转等,全部交由页面控制器处理。

大致结构如下:

final class ProgressCardView: UIView {
    var onInfoTap: (() -> Void)?
    var onRecalculateTap: (() -> Void)?
    var onUnlockTap: (() -> Void)?

    private let infoButton = UIButton(type: .system)
    private let recalculateButton = UIButton(type: .system)
    private let unlockButton = UIButton(type: .system)

    override init(frame: CGRect) {
        super.init(frame: frame)
        infoButton.addTarget(self, action: #selector(infoTapped), for: .touchUpInside)
        recalculateButton.addTarget(self, action: #selector(recalculateTapped), for: .touchUpInside)
        unlockButton.addTarget(self, action: #selector(unlockTapped), for: .touchUpInside)
    }

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

    @objc private func infoTapped() { onInfoTap?() }
    @objc private func recalculateTapped() { onRecalculateTap?() }
    @objc private func unlockTapped() { onUnlockTap?() }
}

这一设计的好处是:后续无论页面流程如何变更,卡片本身无需介入任何业务判断。

tracking 与 completed 两个状态如何落地

通常可按如下方式拆分:

tracking 状态

  • 白色背景卡片
  • 左侧信息图标
  • 中部文案显示剩余天数
  • 下方进度条与日期刻度

completed 状态

  • 高亮卡片背景
  • 完成态文案
  • Recalculate 按钮
  • 主 CTA 按钮

这意味着两种状态共享同一组件入口,但内部布局与交互焦点截然不同。

为何进度条更适合使用 CAGradientLayer

当设计稿中的进度条为渐变色且宽度需动态变化时,强烈推荐使用 CAGradientLayer,而非图片平铺或 patternImage。以下是一个简易实现示例:

final class GradientProgressView: UIView {
    private let trackView = UIView()
    private let fillView = UIView()
    private let gradientLayer = CAGradientLayer()
    private var fillWidthConstraint: NSLayoutConstraint?
    private var progressRatio: CGFloat = 0

    override init(frame: CGRect) {
        super.init(frame: frame)
        trackView.backgroundColor = UIColor(hex: "#ECEBF6")
        trackView.layer.cornerRadius = 5
        trackView.layer.masksToBounds = true
        fillView.layer.cornerRadius = 5
        fillView.layer.masksToBounds = true

        [trackView].forEach {
            $0.translatesAutoresizingMaskIntoConstraints = false
            addSubview($0)
        }
        fillView.translatesAutoresizingMaskIntoConstraints = false
        trackView.addSubview(fillView)
        fillView.layer.addSublayer(gradientLayer)

        NSLayoutConstraint.activate([
            trackView.leadingAnchor.constraint(equalTo: leadingAnchor),
            trackView.trailingAnchor.constraint(equalTo: trailingAnchor),
            trackView.topAnchor.constraint(equalTo: topAnchor),
            trackView.bottomAnchor.constraint(equalTo: bottomAnchor),
            fillView.leadingAnchor.constraint(equalTo: trackView.leadingAnchor),
            fillView.topAnchor.constraint(equalTo: trackView.topAnchor),
            fillView.bottomAnchor.constraint(equalTo: trackView.bottomAnchor)
        ])
        fillWidthConstraint = fillView.widthAnchor.constraint(equalToConstant: 0)
        fillWidthConstraint?.isActive = true

        gradientLayer.colors = [UIColor(hex: "#7B39ED").cgColor, UIColor(hex: "#9B59F0").cgColor]
        gradientLayer.startPoint = CGPoint(x: 0, y: 0.5)
        gradientLayer.endPoint = CGPoint(x: 1, y: 0.5)
    }

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

    override func layoutSubviews() {
        super.layoutSubviews()
        fillWidthConstraint?.constant = trackView.bounds.width * progressRatio
        gradientLayer.frame = fillView.bounds
    }

    func updateProgress(_ ratio: CGFloat) {
        progressRatio = max(0, min(1, ratio))
        fillWidthConstraint?.constant = trackView.bounds.width * progressRatio
        layoutIfNeeded()
    }
}

这种方式最主要的优势是:动态宽度控制更稳定,圆角端点更自然,颜色与渐变方向能精准匹配设计稿。

自定义弹窗为何建议挂载到 window

若项目中已存在自定义 tabbar 或底部持续置顶容器,将 overlay 添加到当前页面 view 上时常常遇到一个问题:弹窗显示、页面变暗,但底部导航依然可见。推荐的解决方案是直接将 overlay 挂载到当前 window

func presentDimOverlay(_ overlay: UIView, from hostView: UIView) {
    guard let window = hostView.window else {
        hostView.addSubview(overlay)
        overlay.frame = hostView.bounds
        return
    }
    window.addSubview(overlay)
    overlay.frame = window.bounds
}

此方法对于“自定义底部导航 + 自定义弹窗”的搭配尤其有效。

一个极易被忽略的问题:显示层不应伪造状态

开发中还遇到一个典型陷阱:进度重置后,视觉上仍显示已完成第1天。原因并非数据未清空,而是 view 层为进度条设置了“最小显示宽度”,导致 0 天 看起来像有部分进度。这里的原则至关重要:若真实状态为 0,则 UI 必须如实显示 0

总结

这类首页状态卡片看似只是一个模块,实则是一个典型的“小型状态系统”。若希望后期易于维护,请务必遵循以下原则:

  1. 显式定义状态枚举,避免依赖大量 isHidden 补丁
  2. 组件仅暴露事件接口,不直接参与业务判断
  3. 渐变进度条优先使用 CAGradientLayer
  4. 全屏 overlay 优先挂载到 window 层级
  5. 显示层绝不可伪造真实状态

一句话总结:优秀的状态卡片并非控件堆砌而成,而是具有清晰状态边界、交互边界和显示边界的组件。

来源:https://juejin.cn/post/7617688223624052745
上一篇搭建RAG系统第一步常见的错误 下一篇从零开始搭建电子书RAG问答系统:Milvus与LangChain实战指南
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
Windows Docker Desktop RabbitMQ生产级部署完整指南
AI教程 · 2026-06-29

Windows Docker Desktop RabbitMQ生产级部署完整指南

前言 在 Windows 本地开发环境中,直接安装 RabbitMQ 确实颇为周折:需要单独配置 Erlang 运行环境、手动管理环境变量、服务启停全凭手工操作。更令人困扰的是,版本兼容冲突、端口占用、环境不一致等问题层出不穷。笔者见过不少开发者为搭建环境就得耗费整整半天时间。 相比之下,借助 Do

AI搜索重构制造业采购逻辑的阿里云企业级GEOCMS优化实践
AI教程 · 2026-06-29

AI搜索重构制造业采购逻辑的阿里云企业级GEOCMS优化实践

先分享一个切实感受。过去两年,我们与福建制造企业合作较为频繁,发现一个非常突出的现象:超过80%的企业官网,产品参数仍然存放在PDF或图片中。AI爬虫?根本无法抓取。这些企业技术实力不弱、资质证照齐全、应用案例也丰富,但在AI搜索这一全新战场上,它们几乎处于隐身状态。 一、一个正在发生的行业变化 A

阿里云Token Plan团队版功能价格与省钱购买指南
AI教程 · 2026-06-29

阿里云Token Plan团队版功能价格与省钱购买指南

阿里云百炼近期推出了名为“Token Plan 团队版”的全新服务,这一服务专为企业与开发者量身打造,定位为AI大模型订阅平台。通过引入Credits作为统一计量单位,将文本生成、图像生成等多模态AI能力纳入单一计费体系,同时无缝兼容主流AI编程工具及智能体(Agent)生态系统。其核心亮点包括:全

阿里云物联网.NET Core客户端位置信息上报
AI教程 · 2026-06-29

阿里云物联网.NET Core客户端位置信息上报

阿里云物联网平台的位置服务并非一个完全独立的功能模块。位置信息可包含二维坐标与三维坐标,而位置数据的来源本质上是借助设备属性进行上传。换言之,若要让设备上报位置,您需先将其视为一个普通属性进行处理。 1)添加二维位置数据 操作过程十分简洁。进入数据分析 → 空间数据可视化 → 二维数据,点击添加,将

年阿里云服务器选型配置与网站部署全攻略
AI教程 · 2026-06-29

年阿里云服务器选型配置与网站部署全攻略

2026年,阿里云服务器生态已高度成熟,形成了清晰的轻量应用服务器与ECS云服务器两大产品阵营。无论你是计划搭建个人博客、企业官网,还是运营电商平台、进行应用开发,基本都能找到理想的解决方案。本指南将从服务器选型、配置选择、部署流程到安全运维,系统梳理2026年最实用的操作要点,帮助你少走弯路,让网