当前位置: 首页
AI教程
iOS首页进度卡开发:状态边界比渐变条更难

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

热心网友 时间:2026-06-26
转载

前言

在健康、训练与打卡类应用中,首页常展示一种“连续 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

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南

科研人员必读:多肽、蛋白质、重组蛋白区别及定制指南

Section 01 多肽 VS 蛋白质 VS 重组蛋白 多肽、蛋白质和重组蛋白,本质上是同宗同源的东西——都是氨基酸串起来的生物大分子。三者的核心区别,说到底无非是三个维度:分子大小、折叠形态,以及生产方式。 接下来是一张清晰的对比图,帮你快速建立直觉: ![对比图1](https:

时间:2026-07-07 17:55
知识图谱与本体语义建模的核心区别解析

知识图谱与本体语义建模的核心区别解析

谈到人工智能如何“理解”知识,有两个概念常被放在一起讨论:知识图谱与本体语义建模。不少人以为它们是同一事物,或者认为后者是前者的进化版。实际上,两者的分工完全不同——打个比方,一个是“记事的本子”,另一个是“写本子之前先定好的规矩”。 1 本体语义建模:先绘制一张“通用分类蓝图” 设想一下,你要整

时间:2026-07-07 17:55
强烈推荐工作搭子WorkBuddy

强烈推荐工作搭子WorkBuddy

一次偶然的机会,从朋友那里了解到WorkBuddy这个工具。说实话,在AI产品扎堆的今天,能遇到一个下载即用的助手,确实值得推荐给每一个被日常琐事缠身的人。 安装过程没什么难度,双击安装包默认安装即可。需要留意的是,如果在Windows7上折腾了半天没反应,别慌——这工具在高版本Windows下运行

时间:2026-07-07 17:55
跨境电商系统自动化测试与CI/CD流水线构建指南

跨境电商系统自动化测试与CI/CD流水线构建指南

技术方向:自动化测试与DevOps实践关键词:日本代购、一站式日淘、雅虎代拍系统、煤炉自动代拍 一、测试分层策略详解 不少人刚开始就想直接搞E2E测试,觉得跑通完整流程才够“真实”。然而,测试金字塔这么多年仍不过时,原因很简单——不同层级的测试各有分工,缺少任何一层都会不稳。来看看这张金字塔图: ┌

时间:2026-07-07 17:54
中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英

中小企业AI营销矩阵工具推荐:赛诺贝斯智域蒲公英

天天刷着别人的爆款内容,自己却“有心无力”——这才是2026年绝大多数中小企业运营社交媒体的真实写照。说白了,社交媒体如今早已不是“要不要做”的选择题,而是“怎么做才能真正见效”的生存考验。现实情况是,团队人力就那么几个,预算也紧巴巴,却要同时运营抖音、小红书、知乎、头条、百家号等多个阵地……文案、

时间:2026-07-07 17:54
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全