SwiftUI之深入解析如何创建一个灵活的选择器
一、前言
- 在 Dribbble 上找到的设计的 SwiftUI 实现时,可以尝试通过一些酷炫的筛选器扩展该项目以缩小结果列表。筛选视图将由两个独立的筛选选项组成,两者都有一些可选项可供选择。但是,在使用 UIKit 时,总是将这种类型的视图实现为具有特定 UICollectionViewFlowLayout 的 UICollectionView。
- 那么,在 SwiftUI 中该如何实现呢?现在来看看使用 SwiftUI 创建灵活选择器的实现。
二、可选择协议
- 选择器的最重要部分是,可以通过该视图组件选择一些所需的选项。因此,首先创建一个 Selectable 协议。所有符合该协议的对象必须实现两个属性:displayedName(在选择器中显示的名称)和 isSelected(一个布尔值,指示特定选项是否已选择)。
- 此外,为了能够通过映射字符串值数组创建 Selectable 对象,实现 Selectable 的对象必须提供带 displayedName 作为参数的自定义初始化。Identifiable 和 Hashable 协议确保我们可以轻松创建具有 ForEach 循环的 SwiftUI 视图。此外,符合 Selectable 协议的所有对象都将实现存储 UUID 值的常量 id。
- 故意省略符合 Selectable 协议的对象的实现,因为这是显而易见的。核心代码如下:
protocol Selectable: Identifiable, Hashable {var displayedName: String { get }var isSelected: Bool { get set }init(displayedName: String)
}
三、自定义化
- 不仅是创建灵活的选择器的实现,还要尽量使其可自定义。因此,将使用符合 Selectable 协议的泛型类型 T 创建 FlexiblePicker,这样,以后更容易重用该组件,因为它将是独立于类型的。
- 在实现选择器本身之前,可以列出所有可自定义属性。接下来,创建用于计算特定字符串值的宽度和高度的字符串扩展。由于允许更改字体大小和权重,因此先前提到的两个扩展都以由灵活选择器使用的 UIFont 作为参数。
extension String {func getWidth(with font: UIFont) -> CGFloat {let fontAttributes = [NSAttributedString.Key.font: font]let size = self.size(withAttributes: fontAttributes)return size.width}func getHeight(with font: UIFont) -> CGFloat {let fontAttributes = [NSAttributedString.Key.font: font]let size = self.size(withAttributes: fontAttributes)return size.height}
}
- 由于字符串扩展用于计算给定字符串的大小,因此需要将所有 UIFont 权重转换为 SwiftUI 等效项。这就是为什么需要引入一个 FontWeight 枚举,其中包含以 UIFont 权重命名的所有可能情况。
- 此外,该枚举有两个属性,一个返回 UIFont 权重,另一个返回 SwiftUI Font 权重。通过这种方式,只需向 FlexiblePicker 提供 FontWeight 枚举的特定情况。
enum FontWeight {case light// the rest of possible casesvar swiftUIFontWeight: Font.Weight {switch self {case .light: return .light// switching through the rest of possible cases }}var uiFontWeight: UIFont.Weight {switch self {case .light: return .light// switching through the rest of possible cases }}
}
四、FlexiblePicker 逻辑
- 之后,终于准备好开始编写 FlexiblePicker 的实现了。首先,需要一个函数来计算并返回输入数据的所有宽度,通过将所有输入值映射到元组中,其中包含输入值和自身的宽度来完成。
- 在映射中,使用 reduce 函数来总结与给定输入值相关联的所有宽度(文本宽度、边框宽度、文本填充和间距)。
private func calculateWidths(for data: [T]) -> [(value: T, width: CGFloat)] {return data.map { selectableType -> (T, CGFloat) inlet font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight.uiFontWeight)let textWidth = selectableType.displayedName.getWidth(with: font)let width = [textPadding, textPadding, borderWidth, borderWidth, spacing].reduce(textWidth, +)return (selectableType, width)}
}
- 现在,计算宽度的函数已经准备好,可以遍历所有输入数据并将它们分成单独的数组,每个数组包含能够适应同一 HStack 中的项目的项目。
- 逻辑很简单,需要有两个数组:
-
- singleLineResult 数组——负责存储适合特定行的项目;
-
- allLinesResult 数组——负责存储所有项目数组(每个数组都等同于一行项目)。
- 首先,检查从 HStack 行宽中减去项宽的结果是否大于 0:
-
- 如果满足条件,将当前项附加到 singleLineResult 中,更新可用的 HStack 行宽,并继续到下一个元素。
-
- 如果结果小于 0,这意味着无法将下一个元素放入给定行中,因此将 singleLineResult 附加到 allLinesResult 中,将 singleLineResult 设置为仅由当前元素组成的数组(不能适应上一行的元素),并通过减去当前项的宽度来更新 HStack 的行宽。
- 在遍历所有元素之后,必须处理特定的边缘情况。singleLineResult 可能不会为空,也不会附加到 allLinesResult 中,因为只在减去项目宽度的结果小于 0 时附加 singleLineResult。在这种情况下,我们必须检查 singleLineResult 是否为空。如果为真,返回 allLinesResult,如果不为真,必须首先附加 singleLineResult,然后返回 allLinesResult。
private func divideDataIntoLines(lineWidth: CGFloat) -> [[T]] {let data = calculateWidths(for: inputData)var singleLineWidth = lineWidthvar allLinesResult = [[T]]()var singleLineResult = [T]()var partialWidthResult: CGFloat = 0data.forEach { (selectableType, width) inpartialWidthResult = singleLineWidth - widthif partialWidthResult > 0 {singleLineResult.append(selectableType)singleLineWidth -= width} else {allLinesResult.append(singleLineResult)singleLineResult = [selectableType]singleLineWidth = lineWidth - width}}guard !singleLineResult.isEmpty else { return allLinesResult }allLinesResult.append(singleLineResult)return allLinesResult
}
- 最后但并非最不重要的是,必须计算 VStack 的高度,以使 SwiftUI 更容易解释我们的视图组件,VStack 的高度是根据两个值计算的:
-
- 输入数据中任何项目的高度(类似于宽度的计算,通过使用 reduce 函数,总结与项目相关的所有高度);
-
- 将显示在 VStack 中的行数。
private func calculateVStackHeight(width: CGFloat) -> CGFloat {let data = divideDataIntoLines(lineWidth: width)let font = UIFont.systemFont(ofSize: fontSize, weight: fontWeight.uiFontWeight)guard let textHeight = data.first?.first?.displayedName.getHeight(with: font) else { return 16 }let result = [textPadding, textPadding, borderWidth, borderWidth, spacing].reduce(textHeight, +)return result * CGFloat(data.count)
}
- 将这两个数字相乘的结果将是我们的 VStack 的高度。
五、FlexiblePicker 视图
- 最后,当所有逻辑准备好后,需要实现一个视图主体。如之前所提到的,视图将使用嵌套的 ForEach 循环创建。需要记住的是,ForEach 循环要求迭代的集合中的每个元素必须符合 Identifiable 协议,或者应该具有唯一的标识符。这就是为什么将分隔行的结果映射到元组中,其中包含每行和 UUID 值。
- 由于如此,可以向 ForEach 循环提供 id 参数。另一点需要记住的是,ForEach 循环期望获得一些 View 作为返回值。如果只插入另一个 ForEach 循环,将在视图的适当功能性方面遇到问题,因为 ForEach 不是一种 View。这就是为什么首先将整个 ForEach 循环包装在 HStack 中,然后再包装在 Group 中,以确保编译器可以正确解释一切。
var body: some View {GeometryReader { geo inVStack(alignment: alignment, spacing: spacing) {ForEach(divideDataIntoLines(lineWidth: geo.size.width).map { (data: $0, id: UUID()) }, id: \.id) { dataArray inGroup {HStack(spacing: spacing) {ForEach(dataArray.data, id: \.id) { data inButton(action: { updateSelectedData(with: data)}) {Text(data.displayedName).lineLimit(1).foregroundColor(textColor).font(.system(size: fontSize, weight: fontWeight.swiftUIFontWeight)).padding(textPadding)}.background(data.isSelected? selectedColor.opacity(0.5): notSelectedColor.opacity(0.5)).cornerRadius(10).disabled(!isSelectable).overlay(RoundedRectangle(cornerRadius: 10).stroke(borderColor, lineWidth: borderWidth))}}}}}.frame(width: geo.size.width, height: calculateVStackHeight(width: geo.size.width))}}
}
- 几乎所有都已经完成,只需添加一个函数来处理与按钮的用户交互,该函数只需切换特定数据的 isSelected 属性:
private func updateSelectedData(with data: T) {guard let index = inputData.indices.first(where: { inputData[$0] == data }) else { return }inputData[index].isSelected.toggle()
}
- 其余的代码很简单,主要是配置所有属性,如字体、颜色或边框。此外,在 VStack 的底部,我们设置一个 frame,其中宽度取自 GeometryReader,高度则由先前创建的函数计算。
- 现在 FlexiblePicker 已经完成,便可以使用了。
六、总结
- 本文完整使用 SwiftUI 构建一个灵活的选择器(FlexiblePicker),用于选择多个选项。
- 首先创建了一个 Selectable 协议,使得选择的选项对象需要实现 displayedName 和 isSelected 属性。
- 然后,详细介绍了实现该选择器的逻辑,包括如何处理选项的布局、宽度和高度,以及如何处理用户与按钮的交互。
- 最后,提供了一个简单的视图实现,可以在 SwiftUI 中使用该选择器,这个选择器可用于创建各种交互式选择界面。
相关文章:
SwiftUI之深入解析如何创建一个灵活的选择器
一、前言 在 Dribbble 上找到的设计的 SwiftUI 实现时,可以尝试通过一些酷炫的筛选器扩展该项目以缩小结果列表。筛选视图将由两个独立的筛选选项组成,两者都有一些可选项可供选择。但是,在使用 UIKit 时,总是将这种类型的视图实…...
【模拟量采集1.2】电阻信号采集
【模拟量采集1.2】电阻信号采集 1 怎么测?2 测输入电阻电压即转为测模拟电压值,这里需要考虑选用怎样的辅助电阻?3 实际电路分析3.1 在不考虑 VCC-5V 电压的纹波等情况时(理想化此时输入的 VCC 就是稳定的 5V)3.2 若考…...
c++牛客总结
一、c/c语言基础 1、基础 1、指针和引用的区别 指针是一个新的变量,指向另一个变量的地址,我们可以通过这个地址来修改该另一个变量; 引用是一个别名,对引用的操作就是对变量本身进行操作;指针可以有多级 引用只有一…...
ts相关笔记(基础必看)
推荐一下小册 TypeScript 全面进阶指南,此篇笔记来源于此,记录总结,加深印象! 另外,如果想了解更多ts相关知识,可以参考我的其他笔记: vue3ts开发干货笔记TSConfig 配置(tsconfig.…...
Docker随笔
OverView 为什么需要Docker 如果我需要部署一个服务,那么我需要提前部署其他应用栈,不同的应用栈会依赖于不用的操作系统和环境。这样做会产生一些负面影响: 不同版本依赖较长的部署时间不同的Dev/Test/Prod环境 这时我们需要一个工具去解…...
uni-app 前后端调用实例 基于Springboot
锋哥原创的uni-app视频教程: 2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中...共计23条视频,包括:第1讲 uni…...
vue3+ts开发干货笔记
总结一下在vue3中ts的使用。当篇记录部分来自于vue官网,记录一下,算是加深印象吧。 纯干笔记,不断补充,想到什么写什么,水平有限,欢迎评论指正! 另外,如果想了解更多ts相关知识&…...
Android开发新的一年Flag
在新的一年里,为了提升Android开发技能,实现更优质的应用程序,我们制定了2024的新年Flag。这些Flag涵盖了技术学习、代码优化、架构升级、用户体验等多个方面,旨在帮助我们成为更优秀的Android开发者。 1. 学习新技术 1.1. Andr…...
好的OODA循环与快慢无关
OODA循环是指观察(Observe)、导向(Orient)、决策(Decide)和行动(Act)这四个步骤的循环过程。它是一种决策和行动的框架,旨在帮助个人或组织更快地适应和应对变化。 OODA循…...
Android 车联网——CarUserService介绍(十三)
一、简介 CarUserService 是 Android 汽车平台的一个组件,它用于管理和提供车辆用户信息。该组件可以让开发者创建和管理与车辆用户相关的数据和配置,包括车辆拥有者和乘客的个人信息、偏好设置、用户偏好配置文件等。 CarUserService 提供了以下功能和特性: 用户配置管理:…...
【开题报告】基于微信小程序的母婴商品仓库管理系统的设计与实现
1.选题背景 随着社会经济的发展和家庭生活水平的提高,母婴商品市场逐渐兴起。然而,传统的母婴商品仓库管理方式存在着许多问题,如信息不透明、操作繁琐等。为了提高仓库管理的效率和准确性,基于微信小程序的母婴商品仓库管理系统…...
分布式锁相关问题(三)
Redis实战精讲-13小时彻底学会Redis 一、什么是分布式锁? 要介绍分布式锁,首先要提到与分布式锁相对应的是线程锁、进程锁。 l 线程锁:主要用来给方法、代码块加锁。当某个方法或代码使用锁,在同一时刻仅有一个线程执行该方法或该…...
grep!Linux系统下强大的文本搜索工具!
grep!Linux系统下强大的文本搜索工具! grep是一个强大的文本搜索工具,它可以在文件中查找包含指定字符串的行。grep的基本语法如下: grep [选项] "搜索字符串" 文件名其中,选项可以是以下几种:…...
(学习打卡1)重学Java设计模式之设计模式介绍
前言:听说有本很牛的关于Java设计模式的书——重学Java设计模式,然后买了(*^▽^*) 开始跟着小傅哥学Java设计模式吧,本文主要记录笔者的学习笔记和心得。 打卡!打卡! 设计模式介绍 一、设计模式是什么? …...
docker 部署教学版本
文章目录 一、docker使用场景及常用命令1)docker使用场景2)rocky8(centos8)安装 docker3)docker 常用命令补充常用命令 二、 单独部署每个镜像,部署spring 应用镜像推荐(2023-12-18)1、 安装使用 mysql1.1 …...
2023春季李宏毅机器学习笔记 05 :机器如何生成图像
资料 课程主页:https://speech.ee.ntu.edu.tw/~hylee/ml/2023-spring.phpGithub:https://github.com/Fafa-DL/Lhy_Machine_LearningB站课程:https://space.bilibili.com/253734135/channel/collectiondetail?sid2014800 一、图像生成常见模型…...
C#和C++存储 和 解析 bin 文件
C 解析 bin 文件 // C 解析 bin 文件 #include <stdio.h>int main() {FILE *file; // 定义文件指针file fopen("example.bin", "rb"); // 打开二进制文件(只读模式)if (file NULL) {printf("无法打开文件\n");re…...
【React系列】Redux(二)中间件
本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. 中间件的使用 1.1. 组件中异步请求 在之前简单的案例中,redux中保存的counter是一个本地定义的数据…...
YOLOv8改进 | 2023Neck篇 | 利用Gold-YOLO改进YOLOv8对小目标检测
一、本文介绍 本文给大家带来的改进机制是Gold-YOLO利用其Neck改进v8的Neck,GoLd-YOLO引入了一种新的机制——信息聚集-分发(Gather-and-Distribute, GD)。这个机制通过全局融合不同层次的特征并将融合后的全局信息注入到各个层级中,从而实现更高效的信息交互和融合。这种…...
ubuntu环境安装配置nginx流程
今天分享ubuntu环境安装配置nginx流程 一、下载安装 1、检查是否已经安装 nginx -v 结果 2、安装 apt install nginx-core 过程 查看版本:nginx -v 安装路径:whereis nginx nginx文件安装完成之后的文件位置: /usr/sbin/nginx…...
【LMM 010】MiniGPT-v2:使用独特的标识符实现视觉语言多任务学习的统一的多模态大模型
论文标题:MiniGPT-v2: Large Language Model As a Unified Interface for Vision-Language Multi-task Learning 论文作者:Jun Chen, Deyao Zhu, Xiaoqian Shen, Xiang Li, Zechun Liu, Pengchuan Zhang, Raghuraman Krishnamoorthi, Vikas Chandra, Yun…...
人工智能如何重塑金融服务业
在体验优先的世界中识别金融服务业中的AI使用场景 人工智能(AI)作为主要行业的大型组织的重要业务驱动力,持续受到关注。众所周知,传统金融服务业在采用新技术方面相对滞后,一些组织使用的还是上世纪50年代和60年代发…...
Iterable 对象转换为 Stream 对象
在 Java 8 中,可以使用 Stream API 来对集合进行操作。要将 Iterable 对象转换为 Stream 对象,可以使用 StreamSupport 类的 stream() 方法。具体来说,可以按照以下步骤进行转换: 调用 Spliterators.spliteratorUnknownSize(iter…...
基于Java+SpringBoot+vue+elementUI私人健身教练预约管理系统设计实现
基于JavaSpringBootvueelementUI私人健身教练预约管理系统设计实现 欢迎点赞 收藏 ⭐留言 文末获取源码联系方式 文章目录 基于JavaSpringBootvueelementUI私人健身教练预约管理系统设计实现一、前言介绍:二、系统设计:2.1 性能需求分析2.2 B/S架构&…...
2024,启动(回顾我的2023)
零.前言 打开博客想写个年度总结,发现已经半年没有更新文章了,排名从几千掉到了几万,不过数据量还是不错的。 时间过得可真快,2023年是充满动荡的一年,上半年gpt横空出世,下半年各种翻车暴雷吃瓜吃到嘴软…...
Web网页开发-盒模型-笔记
1.CSS的三种显示方式 (1)块级元素:标签所占区域默认为一行 特点:一行一个 可设宽高 (2)行内元素:标签所占区域由内容顶开,行内元素无法使用text-align 特点:一行多个 不可设宽高,margin上下和padding上下都不能改变位…...
Java打成压缩包的方法汇总
文章目录 1.将指定目录下的文件打包成 .zip2.将指定目录下的文件打包成 .tar.gz3.将指定目录下的文件打包成 .tar4.将指定目录下的文件打包成 .rar5.生成若干个txt并打包到zip中 1.将指定目录下的文件打包成 .zip 代码示例: import java.io.*; import java.util.z…...
2023年第2季社区Task挑战赛贡献者榜单
基于FISCO BCOS及Weldentity,实现SSO单点登录服务;提供食品溯源、电商运费险7天退保、电子病历等智能合约库业务场景案例;基于FISCO BCOS更新游戏体验;体验并分析解读最新发布的分布式数据协作管理解决方案DDCMS,提供相…...
Clickhouse 为什么快
ClickHouse是一个用于联机分析处理(OLAP)的开源列式数据库管理系统(DBMS)。它之所以能提供出色的查询性能和处理速度,主要归功于以下几个方面的设计和优化: 列式存储 ClickHouse存储数据按列而不是按行组织…...
【React系列】react-router
本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. 认识react-router 1.2. 前端路由原理 前端路由是如何做到URL和内容进行映射呢?监听URL的改变。 UR…...
湖南网站建设360o/百度人工客服在哪里找
项目运行效果 解决方案 SwiftUI 地图视图功能强大,但如果您需要做的不仅仅是显示地图或注释,那么您将不得不研究基于 UIKit 的地图。 在 UIViewRepresentable 的帮助下,SwiftUI 使得将地图从 UIKit 移植到 SwiftUI 世界变得非常容易。 在本文中,我们将使用 UIKit 和 UIVie…...
上海企业网站/竞价sem培训
由于在使用Django的过程当中使用到了Nginx和Fastfds,本文记录一下过程: fastfds架构: 一:安装fastdfs依赖包: 解压缩libfastcommon-master.zip进入到libfastcommon-master的目录中执行 ./make.sh执行 sudo ./make.sh i…...
找个做网站的人/合肥网站维护公司
题目链接:491. 递增子序列 - 力扣(LeetCode)leetcode-cn.com题目描述:给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。示例:输入: [4, 6, 7, 7] 输出: [[4, 6], [4, 7], [4, 6, …...
网站建设资讯/推广平台都有哪些
一个Project 引用 另外 一个Project 显示黄色叹号,后来发现 后一本Project的build设定为.Net4.5, 前一个为4.0, 将版本改为一致后,问题解决。转载于:https://www.cnblogs.com/fdyang/p/4270041.html...
学做网站什么语言合适/深圳关键词优化公司哪家好
问题其实不是最终目的,罗列问题主要是为了找到一些研究的具体方向。作为我自己,就是希望能明确一下接下来深入学习的切入点。推荐系统发展到现在已经得到了广泛的应用,但还存在一些问题。 热点核心问题就是数据稀疏性。很容易理解,…...
大学生创业做网站的筹资方式/小学生简短小新闻
CAN FD描述 随着电动汽车,无人驾驶汽车技术的快速发展,以及对汽车高级驾驶辅助系统和人机交互的增加,传统的CAN总线在传输速率和带宽等方面越来越显得力不从心,因此改进版的CAN总线应运而生。从2012年第13届ICC大会上发布&#x…...