使用 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 cases
    
    var 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) in
        let 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 = lineWidth
    var allLinesResult = [[T]]()
    var singleLineResult = [T]()
    var partialWidthResult: CGFloat = 0
    data.forEach { (selectableType, width) in
        partialWidthResult = singleLineWidth - width
        if 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 in
        VStack(alignment: alignment, spacing: spacing) {
            ForEach(
              divideDataIntoLines(lineWidth: geo.size.width)
                  .map { (data: $0, id: UUID()) }, 
              id: \.id
            ) { dataArray in
                Group {
                    HStack(spacing: spacing) {
                        ForEach(dataArray.data, id: \.id) { data in
                            Button(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 中使用该选择器。这个选择器可用于创建各种交互式选择界面。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

为什么我不能给shopify的图片添加alt

首先我们要明白是什么ALT标签,为什么要添加这个标签,这个标签有什么用 ALT标签是什么 ALT属性是HTML的一部分,它为那些无法查看图像的用户提供替代的文本描述。 ALT标签有什么用 使用ALT属性还可以帮助搜索引擎爬虫更好地理解您的网站内容。有…

OpenSSL 使用AES对文件加解密

AES(Advanced Encryption Standard)是一种对称加密算法,它是目前广泛使用的加密算法之一。AES算法是由美国国家标准与技术研究院(NIST)于2001年发布的,它取代了原先的DES(Data Encryption Stand…

sed命令

目录 一、sed 1.sed命令选项 2.语法选项 3.sed脚本格式 4.搜索替代 5.分组后向引用 1.提取版本号: 2.提取IP地址 3.提取数字权限 6.变量 二、免交互 1.多行重定向 2.免交互脚本 总结:本章主要介绍了seq和免交互的用法及相关知识 一、sed s…

p9 第55题 两个有序单链表L1,L2求交的操作,得到新的链表L3,L3任然保持有序的状态 中国计量大学2016年数据结构题(c语言代码实现)

本题代码如下 linklist merge(linklist* L1, linklist* L2)//将两个链表的公共元素合并产生新链表 {lnode* ra (*L1)->next, * rb (*L2)->next;lnode* r;lnode* s;lnode* C (lnode*)malloc(sizeof(lnode));C->next NULL;r C;while (ra && rb)//循环跳出…

泛微 E-Office sample权限绕过+文件上传组合漏洞Getshell

0x01 产品简介 泛微E-Office是一款标准化的协同 OA 办公软件,泛微协同办公产品系列成员之一,实行通用化产品设计,充分贴合企业管理需求,本着简洁易用、高效智能的原则,为企业快速打造移动化、无纸化、数字化的办公平台。 0x02 漏…

lv11 嵌入式开发 WDT实验 12

目录 1 WDT简介 2 Exynos4412下的WDT控制器 2.1 概述 2.2 WDT的特性 2.3 工作原理 2.4 其他细节 3 WDT寄存器详解 3.1 WTCON控制寄存器 3.2 WTDAT 实时中断寄存器 3.3 WTCNT 递减计数器 3.4 WTCLRINT清除中断寄存器 4 WDT编程 1 WDT简介 Watch Dog Timer即看门狗定…

探索三种生成模型:基于DDPMs、NCSNs和SDEs方法的Diffusion

探索三种生成模型:基于DDPMs、NCSNs和SDEs方法的Diffusion 去噪扩散概率模型(DDPMs)正向过程反向过程 噪声条件得分网络(NCSNs)正向过程初始化训练 NCSNs生成样本 反向过程 随机微分方程(SDEs)原…

自然资源科普交互大屏助力自然资源的保护

在当代社会,自然资源的科学管理和可持续利用变得愈发重要。为了提高公众对于自然资源的认知和理解,科普交互大屏成为一个新兴的工具。它通过生动的图像和实时数据展示,以及与观众的互动方式,让人们更深入地了解自然资源和环境保护…

eNSP防火墙USG6000V使用Web界面登入教程

文章目录 登入流程1、下载USG6000V的镜像包2、导入USG6000V的镜像包3、配置防火墙web页面4、修改本机vmnet1网卡的ipv4地址5、在eNSP上添加云6、配置防火墙管理地址,开启http服务7、关闭电脑防火墙8、访问web页面 登入流程 1、下载USG6000V的镜像包 链接&#xff…

数据库应用:MongoDB 库与集合管理

目录 一、理论 1.MongoDB用户管理 2.MogoDB库管理 3.MogoDB集合管理 二、实验 1.MongoDB用户管理 2.MogoDB库管理 3.MogoDB集合管理 三、问题 1.不显示新创建的数据库 2.插入数据报错 3.删除指定数据库报错 一、理论 1.MongoDB用户管理 (1) 内置角色 数据库用户…

Postman进阶功能实战演练

Postman除了前面介绍的一些功能,还有其他一些小功能在日常接口测试或许用得上。今天,我们就来盘点一下,如下所示: 1.数据驱动 想要批量执行接口用例,我们一般会将对应的接口用例放在同一个Collection中,然…

ZYNQ_project:IIC_EEPROM

EEPROM简介: EEPROM(Electrically Erasable Progammable Read Only Memory, E2PROM)是指带电可擦可编程只读存 储器,是一种常用的非易失性存储器(掉电数据不丢失), E2PROM 有多种类型的产品,我…

【计算机网络学习之路】序列化,反序列化和初识协议

文章目录 前言一. 序列化和反序列化1.自己实现2. JSON 二. 初识协议结束语 前言 本系列文章是计算机网络学习的笔记,欢迎大佬们阅读,纠错,分享相关知识。希望可以与你共同进步。 本篇博文讲解应用层的序列化和反序列化,还有见一…

Linux基础项目开发1:量产工具——显示系统(二)

前言: 前面我们已经对这个项目的基本框架有了一个初步的了解与认识,要实现显示管理器与输入管理器,有输入有输出基本就实现这个项目的大部分功能了,首先我们先来做显示系统,对于上层系统为了让程序更好扩展&#xff0c…

Python分享之字符串格式化 (%操作符)

在许多编程语言中都包含有格式化字符串的功能,比如C和Fortran语言中的格式化输入输出。Python中内置有对字符串进行格式化的操作%。 模板 格式化字符串时,Python使用一个字符串作为模板。模板中有格式符,这些格式符为真实值预留位置&#xff…

【Android Jetpack】Hilt 依赖注入框架

文章目录 依赖注入DaggerHiltKoin添加依赖项Hilt常用注解的含义HiltAndroidAppAndroidEntryPointInjectModuleInstallInProvidesEntryPoint Hilt组件生命周期和作用域如何使用 Hilt 进行依赖注入 依赖注入 依赖注入是一种软件设计模式,它允许客户端从外部源获取其依…

计算机丢失vcomp140.dll是什么意思,如何解决与修复(附教程)

vcomp140.dll缺失的5种解决方法以及vcomp140.dll缺失原因 引言: 在日常使用电脑的过程中,我们可能会遇到一些错误提示,其中之一就是“vcomp140.dll缺失”。这个错误提示通常出现在运行某些程序或游戏时,给使用者带来了困扰。本文…

【蓝桥杯 LCA 差分】 砍树

题目分析: 这道题还是比较裸的一道书上差分的题目了 对于每一对标记点(x,y) 他们之间的路径就是 x − > L C A ( x , y ) − > y x->LCA(x,y)->y x−>LCA(x,y)−>y 这条路径上的每一条边都要经过。 那么对于一条边,什么时候砍掉这条…

【推荐系统】MMOE笔记 20231126

paper阅读 任务差异带来的固有冲突实际上会损害至少某些任务的预测,特别是当模型参数在所有任务之间广泛共享时。(在说ESMM) 共享底层参数可以减少过拟合风险,但是会遇到任务差异引起的优化冲突,因为所有任务都需要在…

Django二转Day02

http #1 http 是什么#2 http特点#3 请求协议详情 -请求首行---》请求方式,请求地址,请求协议版本 -请求头---》key:value形式 -referer:上一次访问的地址 -user-agenet:客户端类型 -name&#x…