SwiftUI 内功加持:“曳光弹“实现自定义样式进度条(ProgressView)

news2025/7/5 5:04:10

在这里插入图片描述

概览

虽然 SwiftUI 已为我们内置了很多常用视图,不过有时我们还是需要根据实际来进一步美化显示或增加功能。

在这里插入图片描述

如上图所示,在本篇博文中我们将结合敏捷哲学中一个超级实用的开发技巧:曳光弹,来一步一个脚印循序渐进的实现 ProgressView 自定义显示效果。

相信看文本后,小伙伴的武器库中又会多了一件非常犀利的神兵利器!

Let’s go!!!😉


从 ProgressViewStyle 开始

在 SwiftUI 中,大多数视图都暴露出对应的 style (以协议的方式)供我们定制它们的外观,比如 Button 的 ButtonStyle。

ProgressView 同样不例外,我们可以通过实现自己的 ProgressViewStyle 样式来完成其外观的美化。

/// A type that applies standard interaction behavior to all progress views
/// within a view hierarchy.
///
/// To configure the current progress view style for a view hierarchy, use the
/// ``View/progressViewStyle(_:)`` modifier.
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
public protocol ProgressViewStyle {

    /// A view representing the body of a progress view.
    associatedtype Body : View

    /// Creates a view representing the body of a progress view.
    ///
    /// - Parameter configuration: The properties of the progress view being
    ///   created.
    ///
    /// The view hierarchy calls this method for each progress view where this
    /// style is the current progress view style.
    ///
    /// - Parameter configuration: The properties of the progress view, such as
    ///  its preferred progress type.
    @ViewBuilder func makeBody(configuration: Self.Configuration) -> Self.Body

    /// A type alias for the properties of a progress view instance.
    typealias Configuration = ProgressViewStyleConfiguration
}

在这里,就让我们创建一个新的 IndicatorProgressViewStyle 样式来施展我们的拳脚吧!

曳光弹(Tracer)原则

在复杂项目开发中,我们一般做不到(或很难做到)一步到位。在敏捷开发哲学中有一个非常实用的秘技,我们称之为:曳(ye)光弹(或拽光弹)!

使用它我们可以由易而难,有条不紊的完成复杂目标。

在这里插入图片描述


关于曳光弹的详细介绍,请小伙伴们移步如下链接观赏:

  • 拽光弹?曳光弹?到底是哪个?为什么能发光,不怕暴露自己吗?

简单来说:我们的开发也要像曳光弹那样不求一发入魂,而是希望通过观察弹道(曳光弹晚上发射时其尾部会拖一道长长的光迹,像彗星美丽的尾部)不断调整射击位置,迭代进步,不断接近目标,最终“征服”目标!

曳光弹的诀窍是,每次只向着目标迈一小步,最终完成一大步!

第一次”射击“

我们的任务是完成如下 ProgressView 样式:

在这里插入图片描述


更详细的介绍,请移步如下链接进一步了解:

  • Working with ProgressView and ProgressViewStyle in SwiftUI

首先,我们先尝试搭建基本布局,当然一切都是静态的:

struct IndicatorProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        VStack {
            Text("30%")
            ZStack(alignment: .topLeading) {
                Capsule(style: .continuous)
                    .foregroundStyle(.gray.opacity(0.5))
                
                Capsule(style: .continuous)
                    .foregroundStyle(.teal.gradient)
                    .frame(width: 100)
            }
            .frame(height: 20)
            
            Text("Downloading...")
        }
    }
}

应用 IndicatorProgressViewStyle 样式很容易:

struct ContentView: View {
    @State var value = 0.3
    
    var body: some View {
        NavigationStack {
            VStack {
                ProgressView(value: value)
                    .progressViewStyle(IndicatorProgressViewStyle())
            }
            .padding()
        }
    }
}

运行效果如下:

在这里插入图片描述

我们第一发曳光弹已经射出,虽然离命中目标还差的很远,但我们起码得到了反馈,看到了希望!

希望就是正能量,希望就是黎明黑暗前那一丝曙光!!!

循序渐进

我们必须想方设法取得进度条的总长度,因为我们需要:

  1. 绘制当前进度对应进度条的长度;
  2. 让顶部指示器始终指向当前进度对应的位置;

有很多种方法可以达到该目的,这里我们选择一种最简单的方法:GeometryReader。

struct IndicatorProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            Text("\(String(format: "%.0f%%", progress * 100))")
            ZStack(alignment: .topLeading) {
                Capsule(style: .continuous)
                    .foregroundStyle(.gray.opacity(0.5))
                    .overlay (
                        GeometryReader { geo in
                            Capsule(style: .continuous)
                                .foregroundStyle(.teal.gradient)
                                .frame(width: geo.size.width * progress, height: 20)
                        }
                    )
            }
            .frame(height: 20)
            
            Text("Downloading...")
        }
    }
}

现在我们的进度条不再是静如处子,而是动如脱兔啦:

在这里插入图片描述

按部就班

接下来,我们再让进度条顶部指示器位置“动”起来。因为它的位置也和当前进度值有着藕断丝连的关系,所以有必要把它也放到 GeometryReader 内部去:

struct IndicatorProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            
            ZStack(alignment: .topLeading) {
                Capsule(style: .continuous)
                    .foregroundStyle(.gray.opacity(0.5))
                    .overlay (
                        GeometryReader { geo in
                            ZStack(alignment: .topLeading) {
                                VStack {
                                    Text("\(String(format: "%.0f%%", progress * 100))")
                                    Image(systemName: "arrowtriangle.down.fill")
                                }.offset(x: 100, y: -40)
                                
                                Capsule(style: .continuous)
                                    .foregroundStyle(.teal.gradient)
                                    .frame(width: geo.size.width * progress, height: 20)
                            }
                        }
                    )
            }
            .frame(height: 20)
            
            Text("Downloading...")
        }
    }
}

在这里插入图片描述

效果如上,随着打出越来越多的曳光弹,我们也越来越接近最终目标!

随后,我们来修正指示器的位置:

struct IndicatorProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            
            ZStack(alignment: .topLeading) {
                Capsule(style: .continuous)
                    .foregroundStyle(.gray.opacity(0.5))
                    .overlay (
                        GeometryReader { geo in
                            ZStack(alignment: .topLeading) {
                                VStack {
                                    Text("\(String(format: "%.0f%%", progress * 100))")
                                    Image(systemName: "arrowtriangle.down.fill")
                                }
                                .frame(width: 40)
                                .offset(x: geo.size.width * progress - 20, y: -40)
                                
                                Capsule(style: .continuous)
                                    .foregroundStyle(.teal.gradient)
                                    .frame(width: geo.size.width * progress, height: 20)
                            }
                        }
                    )
            }
            .frame(height: 20)
            
            Text("Downloading...")
        }
    }
}

现在,我们的指示器也能够跟随进度条一起“如影随形”的移动啦:

在这里插入图片描述

一个小问题

不知细心的小伙伴们发现了吗?目前的实现有点小缺陷:当进度位于开始位置时,进度条显示的高度不太对:
在这里插入图片描述

这是因为我们的进度条是用 Capsule 形状来绘制的,而 Capsule 在宽度很窄时显示会趋于一个矩形,而且它被绘制在进度槽的外部。

如何解决呢?很简单,我们只需将进度条绘制限制在指定的 Capsule 内部就可以了:

struct IndicatorProgressViewStyle: ProgressViewStyle {
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            
            ZStack(alignment: .topLeading) {
                // 原代码从略...
            }
            .frame(height: 20)
            // 限制进度条在指定形状中显示
            .clipShape(Capsule(style: .continuous))
            
            Text("Downloading...")
        }
    }
}

在这里插入图片描述

效果好多了!

不过将进度条限制为指定形状,意味着其它部分(比如指示器)都会被裁剪掉,这可不是我们想要的。

随着更多曳光弹的射出,我们也对如何解决这一问题有了更多的线索,只需调整进度条指示器的位置即可:

struct IndicatorProgressViewStyle: ProgressViewStyle {
    
    private let labelWidth = 40.0
    private let viewHeight = 20.0
        
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            Capsule(style: .continuous)
                .opacity(0.0)
                .overlay {
                    GeometryReader { geo in
                        VStack {
                            VStack {
                                Text("\(String(format: "%.0f%%", progress * 100))")
                                Image(systemName: "arrowtriangle.down.fill")
                            }
                            .font(.headline)
                            .frame(width: labelWidth)
                            .offset(x: progress * geo.size.width - labelWidth / 2, y: -40)
                        }
                        
                        ZStack(alignment: .topLeading) {
                            Capsule(style: .continuous)
                                .foregroundStyle(.gray.opacity(0.5))
                            
                            Capsule(style: .continuous)
                                .foregroundStyle(.teal.gradient)
                                .frame(width: progress * geo.size.width)
                        }
                        .clipShape(Capsule(style: .continuous))
                    }
                }
                .frame(height: viewHeight)
            
            Text("Downloading...")
        }
    }
}

看看效果吧,我们已经快逼近最终目标了!

在这里插入图片描述

增加更多的定制项

现在,我们自定义进度条样式对于顶部指示器和底部描述的显示有着自己的“想法”,能不提供更多的自由让使用者决定如何显示呢?

答案是肯定的!

在 ProgressView 构造器中包含 label 和 currentValueLabel 可选参数,我可以用它们来决定进度条指示器和描述如何显示!

struct IndicatorProgressViewStyle: ProgressViewStyle {
    
    private let labelWidth = 40.0
    private let viewHeight = 20.0
        
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            Capsule(style: .continuous)
                .opacity(0.0)
                .overlay {
                    GeometryReader { geo in
                        VStack {
                            VStack {
                                if let curLabel = configuration.currentValueLabel {
                                    curLabel
                                }else{
                                    Text("\(String(format: "%.0f%%", progress * 100))")
                                }
                                Image(systemName: "arrowtriangle.down.fill")
                            }
                            .frame(width: labelWidth)
                            .offset(x: progress * geo.size.width - labelWidth / 2, y: -40)
                        }
                        
                        ZStack(alignment: .topLeading) {
                            Capsule(style: .continuous)
                                .foregroundStyle(.gray.opacity(0.5))
                            
                            Capsule(style: .continuous)
                                .foregroundStyle(.teal.gradient)
                                .frame(width: progress * geo.size.width)
                        }
                        .clipShape(Capsule(style: .continuous))
                    }
                }
                .frame(height: viewHeight)
            
            configuration.label
        }
    }
}

至此,我们可以自由定制 ProgressView 的显示外观啦:

ProgressView(value: value, label: {
    Text(progressDetails)
        .font(.title3.weight(.bold))
        .foregroundStyle(.green.gradient)
}, currentValueLabel: {
    Text("\(String(format: "🚴🏻‍♂️%.0f%%", value * 100))")
        .font(.headline)
        .frame(width: 60)
        .foregroundStyle(Color.orange.gradient)
})
.progressViewStyle(IndicatorProgressViewStyle(color: AnyShapeStyle(.pink.gradient)))
.frame(width: 300)

让我们看一下美美哒的最终效果吧:

在这里插入图片描述

完成品

以下是细微完善过的最终源代码:

struct IndicatorProgressViewStyle: ProgressViewStyle {
    
    private let labelWidth = 40.0
    private let viewHeight = 20.0
    
    var color: AnyShapeStyle?
        
    func makeBody(configuration: Configuration) -> some View {
        
        let progress = configuration.fractionCompleted ?? 0.0
        
        VStack {
            Capsule(style: .continuous)
                .opacity(0.0)
                .overlay {
                    GeometryReader { geo in
                        VStack {
                            VStack {
                                if let curLabel = configuration.currentValueLabel {
                                    curLabel
                                }else{
                                    Text("\(String(format: "%.0f%%", progress * 100))")
                                }
                                Image(systemName: "arrowtriangle.down.fill")
                            }
                            .minimumScaleFactor(0.8)
                            .font(.headline)
                            .frame(width: labelWidth)
                            .offset(x: progress * geo.size.width - labelWidth / 2, y: -40)
                        }
                        
                        ZStack(alignment: .topLeading) {
                            Capsule(style: .continuous)
                                .foregroundStyle(Color.gray.opacity(0.3))
                            
                            Capsule(style: .continuous)
                                .foregroundStyle(color != nil ? color! : AnyShapeStyle(.teal.gradient))
                                .frame(width: progress * geo.size.width)
                                .shadow(radius: 3.0)
                        }
                        .clipShape(Capsule(style: .continuous))
                    }
                }
                .frame(height: viewHeight)
            
            configuration.label
        }
    }
}

总结

在本篇博文中,我们利用敏捷开发中“曳光弹”原则步步为营打造了一款漂亮的进度条样式;我们最终完美的实现了目标,更重要的是:我们学到了软件工程中重要的一课!

感谢观赏,再会!!!😎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/995806.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

应急响应-Windows挖矿实战

0x00 主机表现 windows主机cpu拉满,主机卡顿,初步判断为中了挖矿病毒 0x00 处置 通过cpu拉满状态,定位初步的进程文件, 通过进程得到的文件上传沙箱,结果显示为恶意文件, 定位到文件夹, 存…

力扣:92. 反转链表 II(Python3)

题目&#xff1a; 给你单链表的头指针 head 和两个整数 left 和 right &#xff0c;其中 left < right 。请你反转从位置 left 到位置 right 的链表节点&#xff0c;返回 反转后的链表 。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 链接&#xff1a;力扣&#…

秋招实习 算法刷题网站推荐

Home - CodeFun2000 优点&#xff1a; 收录了各大互联网公司最新最全的笔试题。平台贴合真实笔试环境&#xff0c;都是Acm模式&#xff0c;有利于准备秋招。网站的每题都有coder提供的题解&#xff0c;也有专门的博客对每种类型的题目进行分类&#xff0c;大大提高学习的效率。…

Nginx 配置错误导致漏洞

文章目录 Nginx 配置错误导致漏洞1. 环境启动2. CRLF注入漏洞2.1 漏洞描述2.2 漏洞原理2.3 漏洞利用2.4 修复建议 3. 目录穿越漏洞3.1 漏洞描述3.2 漏洞原理3.3 漏洞利用3.4 修复建议 4. add_header被覆盖4.1 漏洞描述4.2 漏洞原理4.3 漏洞利用4.4 修复建议 Nginx 配置错误导致…

矩阵论—线性子空间、生成子空间、核空间、零度、子空间的交与和、直和

线性子空间定义 如果&#xff0c;V1称为平凡子空间&#xff0c;否则称为非平凡子空间。 生成子空间 核空间、零度 解&#xff1a; rank(A)2; n(A)N-rank(A)3-21&#xff0c;这里N表示的是未知量的个数。 n(A)也可以理解为基础解系的个数&#xff0c;即基础解系中有几个向量…

基于TensorFlow 2.3.0 的手势识别系统设计

一、开发环境 Windows 10PyCharm 2021.3.2Python 3.7TensorFlow 2.3.0 二、制作数据集&#xff0c;作者使用了10个类别的手势数集据 三、开始训练模型&#xff0c;作者使用自己开发的软件进行训练模型&#xff0c;方便快捷。软件介绍及下载地址&#xff1a; 手把手教你使用T…

基于SpringBoot+微信小程序的智慧医疗线上预约问诊小程序

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 近年来&#xff0c;随…

【Python】Python实现五子棋游戏(带可视化界面)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;公众号&#x1f448;&#xff1a;测试开发自动化【获取源码商业合作】 &#x1f449;荣__誉&#x1f448;&#xff1a;阿里云博客专家博主、5…

信息安全技术 办公设备安全测试方法

声明 本文是学习GB-T 38558-2020 信息安全技术 办公设备安全测试方法. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 办公设备安全测试方法范围 本标准规定了办公设备安全技术要求和安全管理功能要求的测试方法。 本标准适用于测试机构、办公设备厂…

云备份服务端——服务器业务处理模块以及网络通信模块

我们这里由于网络通信模块借助httplib库来完成&#xff0c;因此两个模块合并到一起完成&#xff0c;不熟悉httplib库的老铁可以再看看我之前的文章 云备份——第三方库使用介绍&#xff08;下&#xff09;_爱吃鱼的修猫的博客-CSDN博客 一&#xff0c;业务处理模块设计 我们这里…

UMA 2 - Unity Multipurpose Avatar☀️七.UMA API介绍 : 基本API与保存加载配置

文章目录 🟥 UMA Data DNA参数引用位置🟥 UMA API介绍🟥 UMA Data DNA参数引用位置 我们想通过代码去控制如图所示参数,达到捏脸的目的.下面就是可以控制的代码: _dna["headSize"].Set(1); _avatar.BuildCharacter();我们观察发现操控代码类似Material去设置…

【MySQL】一文详解MySQL,从基础概念到调优

作者简介 前言 博主之前写过一个MySQL的系列&#xff0c;从基础概念、SQL到底层原理、优化&#xff0c;专栏地址&#xff1a; https://blog.csdn.net/joker_zjn/category_12305262.html?spm1001.2014.3001.5482 本文会是这个系列的清单&#xff0c;拉通来聊一聊Mysql从基础概…

MT3905替代方案 (NDP23511KC)完全替代MT3905

NDP23511KC效率高&#xff0c; 单片同步降压DC/DC 变频器采用恒频&#xff0c; 平均电流模式控制架构。 能够提供高达5.0A的峰值负载 具有优良的线路和负载调节。的 设备从输入电压工作 范围为4.6V至30V&#xff0c;并提供一个 输出电压从3.3V到25V可调。 NDP23511KC特…

2023数学建模国赛A题定日镜场的优化设计- 全新思路及代码

背景资料关键信息和要点如下&#xff1a; 定日镜&#xff1a;塔式太阳能光热发电站的基本组件&#xff0c;由纵向转轴和水平转轴组成&#xff0c;用于反射太阳光。 定日镜场&#xff1a;由大量的定日镜组成的阵列。 集热器&#xff1a;位于吸收塔顶端&#xff0c;用于收集太…

UG\NX CAM二次开发 设置几何体自动毛坯 UF_CAM_set_auto_blank

文章作者:代工 来源网站:NX CAM二次开发专栏 简介: UG\NX CAM二次开发 设置几何体自动毛坯 UF_CAM_set_auto_blank 效果: 代码: void MyClass::do_it(){ //获取加工环境tagtag_t setup_tag=NULL_TAG;UF_SETUP_ask_setup(&setup_tag);//返回当前工序导航器…

python3网络爬虫--2323爬取B站视频弹幕 解so文件(附源码)

文章目录 一&#xff0e;前言二&#xff0e;配置Protobuf 环境&生成编译文件1&#xff0e;配置Protobuf 环境2&#xff0e;生成编译文件 三&#xff0e;解析弹幕四&#xff0e;自动解析弹幕五&#xff0e;总结六&#xff0e;参考 本篇博文记录一下爬取B站弹幕的主要思路以及…

NFS文件共享系统(K8S)

概述 部署NFS文件共享服务&#xff0c;为Kubernetes提供NFS共享做准备 步骤 安装软件 yum -y install nfs-utils 配置NFS(exports) 编辑 /etc/exports 文件。每一行代表一个共享目录&#xff0c;描述目录如何共享 编写规则&#xff1a; # <共享目录> [客户端1 选项…

Spring修炼之路--基础知识

一、核心概念 1.1软件模块化 软件模块化是一种软件开发的设计模式&#xff0c;它将一个大型的软件系统划分成多个独立的模块&#xff0c;每个模块都有自己的功能和接口&#xff0c;并且能够与其他模块独立地工作1. 软件模块化设计可以使软件不至于随着逐渐变大而变得不可控&am…

​ICCV 2023 | 图像分割全新思路:仅用文本描述实现图像分割!

论文链接&#xff1a; https://arxiv.org/pdf/2308.14575.pdf 代码链接&#xff1a; https://github.com/fawnliu/TRIS 基本概念&#xff1a;Referring Image Segmentation&#xff08;RIS&#xff09;是一种图像分割技术&#xff0c;旨在根据自然语言表达来标记图像或视频中表…

教师节 | 拓世AI这样用,教案不再费神,辅助教师教学全流程

随着科技的不断进步&#xff0c;人工智能和大数据技术所带来的新的信息技术革命在每个细分领域生根发芽&#xff0c;教育领域也不例外。这个新时代的教育充满了挑战和机遇&#xff0c;科技的发展正引领着教育走向一个全新的境界。 教育是社会进步的重要引擎&#xff0c;教师是…