使用 SwiftUI 创建一个灵活的选择器

在这里插入图片描述

文章目录

    • 前言
    • 可选择协议
    • 自定义化
    • FlexiblePicker 逻辑
    • FlexiblePicker 视图
    • 总结

前言

最近,在我正在开发一个在 Dribbble 上找到的设计的 SwiftUI 实现时,我想到了一个点子,可以通过一些酷炫的筛选器扩展该项目以缩小结果列表。

我决定筛选视图将由两个独立的筛选选项组成,两者都有一些可选项可供选择。但然后我遇到了一个问题。在使用 UIKit 时,我总是将这种类型的视图实现为具有特定 UICollectionViewFlowLayoutUICollectionView。但在 SwiftUI 中该如何实现呢?

让我们来看看使用 SwiftUI 创建灵活选择器的实现!

可选择协议

选择器的最重要部分是,我们可以通过该视图组件选择一些所需的选项。因此,首先创建了一个 Selectable 协议。

所有符合该协议的对象必须实现两个属性:displayedName(在选择器中显示的名称)和 isSelected(一个布尔值,指示特定选项是否已选择)。

此外,为了能够通过映射字符串值数组创建 Selectable 对象,实现 Selectable 的对象必须提供带 displayedName 作为参数的自定义初始化。

IdentifiableHashable 协议确保我们可以轻松创建具有 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 协议,使得选择的选项对象需要实现 displayedNameisSelected 属性。

然后,详细介绍了实现该选择器的逻辑,包括如何处理选项的布局、宽度和高度,以及如何处理用户与按钮的交互。

最后,提供了一个简单的视图实现,可以在 SwiftUI 中使用该选择器。这个选择器可用于创建各种交互式选择界面。

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

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

相关文章

C++二分查找、离线算法:最近的房间

作者推荐 利用广度优先或模拟解决米诺骨牌 本文涉及的基础知识点 二分查找算法合集 题目 一个酒店里有 n 个房间,这些房间用二维整数数组 rooms 表示,其中 rooms[i] [roomIdi, sizei] 表示有一个房间号为 roomIdi 的房间且它的面积为 sizei 。每一…

【数值计算方法(黄明游)】常微分方程初值问题的数值积分法:欧拉方法(向前Euler)【理论到程序】

文章目录 一、数值积分法1. 一般步骤2. 数值方法 二、欧拉方法(Euler Method)1. 向前欧拉法(前向欧拉法)a. 基本理论b. 典例解析c. 算法实现 常微分方程初值问题的数值积分法是一种通过数值方法求解给定初始条件下的常微分方程&am…

【安装指南】MySQL和Navicat下载、安装及使用详细教程

目录 ⛳️1.【MySQL】安装教程 1.1 获取下载包 1.2 MySQL安装 1.2.1 MySQL工具安装 1.2.2 MySQL环境变量 1.2.3 验证MySQL安装成功 ⛳️2.【Navicat-v15】的安装和无限使用 ⛳️3.【测试Navicat连接MySQL】 ⛳️1.【MySQL】安装教程 1.1 获取下载包 前往官网获取压缩包…

UDP实现群聊通信

服务器端 #include <myhead.h> #define UDPIP "192.168.115.92" #define UDPPORT 6666 //存储客户信息的链表结构体 typedef struct Node {char name[20];struct sockaddr_in cin;struct Node *next; }*linklist; //数据结构体 struct data_cli {char type;ch…

分类预测 | Matlab实现NGO-KELM北方苍鹰算法优化核极限学习机分类预测

分类预测 | Matlab实现NGO-KELM北方苍鹰算法优化核极限学习机分类预测 目录 分类预测 | Matlab实现NGO-KELM北方苍鹰算法优化核极限学习机分类预测分类效果基本描述程序设计参考资料 分类效果 基本描述 1.Matlab实现NGO-KELM北方苍鹰算法优化核极限学习机分类预测&#xff08;完…

只会在终端使用Python运行代码?这些高级用法了解了解

大部分同学在终端使用Python可能只是简单的执行代码&#xff0c;但其实结合一些Python内置模块或第三方库可以实现更高级且便捷的用法&#xff0c;一起看看吧 插播&#xff0c;更多文字总结指南实用工具科技前沿动态第一时间更新在公粽号【啥都会一点的研究生】 代码Benchmar…

【代码】基于卷积神经网络(CNN)-支持向量机(SVM)的分类预测算法

程序名称&#xff1a;基于卷积神经网络&#xff08;CNN&#xff09;-支持向量机&#xff08;SVM&#xff09;的分类预测算法 实现平台&#xff1a;matlab 代码简介&#xff1a;CNN-SVM是一种常用的图像分类方法&#xff0c;结合了卷积神经网络&#xff08;CNN&#xff09;和支…

绝地求生:成长型皮肤异色定价是否有些夸张?

大家好&#xff0c;我闲游盒小盒子&#xff01; 自从26.2更新上架回归的黑市中四款成长型皮肤以后&#xff0c;能看到社区里很多玩家都分享抽中了自己心仪的成长型皮肤。 但是对于异色很少有人去实装&#xff0c;大多数玩家都是选择去分解异色换取五张图纸然后追求升级原皮等级…

【古月居《ros入门21讲》学习笔记】15_ROS中的坐标系管理系统

目录 说明&#xff1a; 1. 机器人中的坐标变换 tf功能包能干什么&#xff1f; tf坐标变换如何实现 2. 小海龟跟随实验 安装 ros-melodic-turtle-tf 实验命令 运行效果 说明&#xff1a; 1. 本系列学习笔记基于B站&#xff1a;古月居《ROS入门21讲》课程&#xff0c;且使…

怎么更新BI报表数据?问我就对了

BI大数据分析工具上有大量的BI报表模板&#xff0c;这些模板都是一个个完整的BI报表&#xff0c;只需将数据源更换&#xff0c;立即就能用来分析我们自己的数据。那&#xff0c;BI报表的数据怎么更新&#xff1f;接下来就来说说这事。 目的&#xff1a;更新BI报表数据 工具&a…

Flask教程入门

1.学习Flask之前&#xff0c;首先需要对URL进行一定的了解。 URL的一些知识&#xff1a; 1.URL只能包含ASCII码里面一些可显示的字符&#xff0c;如A-Z&#xff0c;a-z&#xff0c;0-9&#xff0c;&&#xff0c;#&#xff0c;%&#xff0c;&#xff1f;&#xff0c;/等字符…

vue项目中使用jsonp跨域请求百度联想接口

一. 内容简介 vue项目中使用jsonp跨域请求百度联想接口 二. 软件环境 2.1 Visual Studio Code 1.75.0 2.2 chrome浏览器 2.3 node v18.14.0 三.主要流程 3.1 代码 核心代码 // 这个是请求函数doLeno() {// 挂载回调函数&#xff0c;不挂载&#xff0c;会报不存在window…