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…...

Spark 之 入门讲解详细版(1)
1、简介 1.1 Spark简介 Spark是加州大学伯克利分校AMP实验室(Algorithms, Machines, and People Lab)开发通用内存并行计算框架。Spark在2013年6月进入Apache成为孵化项目,8个月后成为Apache顶级项目,速度之快足见过人之处&…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...

理解 MCP 工作流:使用 Ollama 和 LangChain 构建本地 MCP 客户端
🌟 什么是 MCP? 模型控制协议 (MCP) 是一种创新的协议,旨在无缝连接 AI 模型与应用程序。 MCP 是一个开源协议,它标准化了我们的 LLM 应用程序连接所需工具和数据源并与之协作的方式。 可以把它想象成你的 AI 模型 和想要使用它…...
MVC 数据库
MVC 数据库 引言 在软件开发领域,Model-View-Controller(MVC)是一种流行的软件架构模式,它将应用程序分为三个核心组件:模型(Model)、视图(View)和控制器(Controller)。这种模式有助于提高代码的可维护性和可扩展性。本文将深入探讨MVC架构与数据库之间的关系,以…...

学习STC51单片机31(芯片为STC89C52RCRC)OLED显示屏1
每日一言 生活的美好,总是藏在那些你咬牙坚持的日子里。 硬件:OLED 以后要用到OLED的时候找到这个文件 OLED的设备地址 SSD1306"SSD" 是品牌缩写,"1306" 是产品编号。 驱动 OLED 屏幕的 IIC 总线数据传输格式 示意图 …...

WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...

CVE-2020-17519源码分析与漏洞复现(Flink 任意文件读取)
漏洞概览 漏洞名称:Apache Flink REST API 任意文件读取漏洞CVE编号:CVE-2020-17519CVSS评分:7.5影响版本:Apache Flink 1.11.0、1.11.1、1.11.2修复版本:≥ 1.11.3 或 ≥ 1.12.0漏洞类型:路径遍历&#x…...
CRMEB 中 PHP 短信扩展开发:涵盖一号通、阿里云、腾讯云、创蓝
目前已有一号通短信、阿里云短信、腾讯云短信扩展 扩展入口文件 文件目录 crmeb\services\sms\Sms.php 默认驱动类型为:一号通 namespace crmeb\services\sms;use crmeb\basic\BaseManager; use crmeb\services\AccessTokenServeService; use crmeb\services\sms\…...

接口自动化测试:HttpRunner基础
相关文档 HttpRunner V3.x中文文档 HttpRunner 用户指南 使用HttpRunner 3.x实现接口自动化测试 HttpRunner介绍 HttpRunner 是一个开源的 API 测试工具,支持 HTTP(S)/HTTP2/WebSocket/RPC 等网络协议,涵盖接口测试、性能测试、数字体验监测等测试类型…...

C# 表达式和运算符(求值顺序)
求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如,已知表达式3*52,依照子表达式的求值顺序,有两种可能的结果,如图9-3所示。 如果乘法先执行,结果是17。如果5…...