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 中使用该选择器,这个选择器可用于创建各种交互式选择界面。

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

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

相关文章

RK3568 学习笔记 : ubuntu 20.04 下 Linux-SDK 镜像烧写

前言 开发板:【正点原子】ATK-DLRK3568 开发板,编译完 Linux-SDK 后,生成了相关的镜像文件,本篇记录一下 镜像烧写,当前编译环境在 VMware 虚拟机中,虚拟机系统是 ubuntu 20.04 此次烧写还算顺利&#xff…

Callback Hook

一、Callback Hook 函数名:useCallback 用于得到一个固定引用值的函数,通常用它进行性能优化。 useCallback: 该函数只需要传入两个参数:一个回调函数和一个依赖数组即可。 1.函数,useCallback会固定该函数的引用,…

搜索二维矩阵 II(LeetCode 240)

1.问题描述 编写一个高效的算法来搜索 m x n 矩阵 matrix 中的一个目标值 target 。该矩阵具有以下特性: 每行的元素从左到右升序排列。每列的元素从上到下升序排列。 示例: 输入:matrix [[1,4,7,11,15],[2,5,8,12,19],[3,6,9,16,22],[10…

快速、准确地检测和分类病毒序列分析工具 ViralCC的介绍和详细使用方法,fudai shiyong ijaoben

介绍 viralcc是一个基因组病毒分析工具,可以用于快速、准确地检测和分类病毒序列。 github:dyxstat/ViralCC: ViralCC: leveraging metagenomic proximity-ligation to retrieve complete viral genomes (github.com) Instruction of reproducing resul…

微众区块链观察节点的架构和原理 | 科普时间

践行区块链公共精神,实现更好的公众开放与监督!2023年12月,微众区块链观察节点正式面向公众开放接入功能。从开放日起,陆续有多个观察节点在各地运行,同步区块链数据,运行区块链浏览器观察检视数据&#xf…

flutter项目用vscode打包apk包,完美运行到手机上

1.创建密钥库 执行以下命令: keytool -genkey -v -keystore F:/key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias key 生成 2,048 位RSA密钥对和自签名证书 (SHA256withRSA) (有效期为 10,000 天) 2.填写密钥内容 执行以上命令后会提示一次输入密钥库密码、确认…

【ES6语法学习】解构赋值

文章目录 引言一、什么是解构赋值1.1什么是解构赋值1.2 数组的解构赋值1.2.1 基本用法1.2.2 默认值1.2.3 剩余参数 1.3 对象的解构赋值1.3.1 基本用法1.3.2 默认值1.3.2 剩余参数 1.4 字符串的解构赋值1.5 函数参数的解构赋值 二、解构赋值的优势和应用场景2.1 代码简化和可读性…

【React系列】JSX核心语法和原理

本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__bizMzg5MDAzNzkwNA&actiongetalbum&album_id1566025152667107329) 一. ES6 的 class 虽然目前React开发模式中更加流行hooks,但是依然有很多的项目依然是使用类组件&#x…

es索引数据过滤查询

1.我们往kibana插入数据,来进行查询 POST /t1/_doc/ {"name":"cat","age":"18","address":"BJ","job":"dev" } POST /t1/_doc/ {"name":"dog","age":"1…

改善 GitHub Pages 阅读体验:Quick Docs

一个不到 10MB 的小工具,来提供本地、快速的文档访问,来改善开发过程中,阅读在线文档体验糟糕的问题。 以及,介绍如何快速制作一个利于分发使用的,离线文档工具包。 写在前面 即使现在 AI 辅助编码和 Chat Bot 类的…

【git使用】了解三种git commit合并的使用场景(rebase、merge、cherry-pick)

参考 【Git学习笔记】逃不掉的merge和rebase-腾讯云开发者社区-腾讯云git merge 和 git rebase - 知乎git cherry-pick 教程 - 阮一峰的网络日志 简单理解各种合并的方法 线性合并,使用 rebase —— feature 分支开发,提交前拉取 master 最新改动进行…

初识HTTP协议

Web服务器可以接收浏览器的请求,并将服务器中的web项目资源响应给浏览器,浏览器与服务器之间进行网络通信遵循HTTP协议。 一、什么是HTTP协议 超文本传输协议(HTTP,HyperText Transfer Protocol)(浏览器---->web服务…

微信小程序:flex常用布局

在我们平时微信小程序开发过程中为了页面能达到设计小伙伴的预期,追求还原度,那我们肯定会使用很多常用的布局方式,那我们今天就介绍一下微信小程序中常用的一些flex布局 1、常用flex布局 /** 水平垂直居中 **/ .flex-center {display: fle…

vue3+vant4 移动端软键盘弹出 收起导致项目样式布局错乱解决方案,亲测有效!!

问题描述 最近在做vue3 H5的移动端项目 我用的是vue3vant4,然后在使用过程中发现 小米14手机在点击密码输入框软键盘弹出 时会导致项目布局整体向上移动 导致页面布局错乱。 原因分析: 在移动端软键盘弹出收起时,导致项目样式布局错乱的原因…

基于STM32的ESP8266 WiFi模块数据采集与显示

基于STM32的ESP8266 WiFi模块数据采集与显示是一种常见的嵌入式系统应用,通常用于远程数据监测和控制。在这种应用中,STM32作为主控制器负责采集周围环境的数据,通过ESP8266 WiFi模块将数据发送到远程服务器,并在远程服务器上进行…

03-微服务-Ribbon负载均衡

Ribbon负载均衡 1.1.负载均衡原理 SpringCloud底层其实是利用了一个名为Ribbon的组件,来实现负载均衡功能的。 那么我们发出的请求明明是http://userservice/user/1,怎么变成了http://localhost:8081的呢? 1.2.源码跟踪 为什么我们只输入…

小梅哥Xilinx FPGA学习笔记20——无源蜂鸣器驱动设计与验证(音乐发生器设计)

目录 一:章节导读 二:无源蜂鸣器驱动原理 三:PWM 发生器模块设计 3.1 PWM 发生器模块框图 3.2 PWM 发生器模块接口功能描述 3.3 PWM波生成设计文件代码 3.4 测试仿真文件 3.5 测试仿真结果 3.6 板级调试与验证之顶层文件设计 四&am…

Python:界面开发,wx入门篇

以下内容为本人的学习笔记,如需要转载,请声明原文链接 微信公众号「ENG八戒」https://mp.weixin.qq.com/s/3Yb_YAKiMte_f5HanetXiA 本文大概 3617 个字,阅读需花 10 分钟 内容不多,但也花了一些精力 如要交流,欢迎评…

@Scheduled定时任务现状与改进

项目场景: 定时任务现状:每个项目都会有一些配置信息,这些信息我们是都放在一个配置服务中,这个服务会定时从配置表中加载所有配置存入本地JVM内存,以供调用方获取(调用方集成了配置服务的SDK,…

elasticsearch的查询方式和数据库事务隔离级别的思考

项目中用到了 elasticsearch,发现有几种查询方式不太一样,思考了一下,总结如下 普通分页 等同于关系数据库的分页查询,例如 mysql 的 limit,如下 sql select * from test limit 100000,10 这种查询方式有一个问题&a…