SwiftUI的多列列表
源代码:https://github.com/chr1s78/MultipleColumnsListSwiftUI/tree/main
尝试手动实现一个SwiftUI的多列列表,效果如下:
在列表的标题滚动到顶部时,显示对应的标题内容,并固定,实现此方法需要用到 preference 首选项功能。关于preference,可以参阅这篇文章 The magic of view preferences in SwiftUI
简单来说,preference允许我们将子视图的属性,传递给父视图。在这个例子中,我们将标题栏的位置和大小,传递给父视图,由父视图来判断当前需要显示哪个标题栏。
我们要做的具体内容为:
1、固定显示第一个标题栏
2、获得所有标题栏的尺寸和位置
3、在滚动时判断是否有标题栏滚动到了固定的第一个标题栏的位置
4、显示对应的标题栏内容
首先定义一个preferenceKey
struct ListRowPreferenceKey: PreferenceKey {typealias Value = [ListRowPreferenceData]static var defaultValue: [ListRowPreferenceData] = []static func reduce(value: inout [ListRowPreferenceData], nextValue: () -> [ListRowPreferenceData]) {value.append(contentsOf: nextValue())}
}struct ListRowPreferenceData: Equatable {let rect: CGRect
}
ListRowPreferenceData中,存放需要记录的子视图的位置数据
我们将列表划分为两种视图组成,标题栏视图和列表内容视图
标题栏视图代码如下
// MARK: 列表标题行
struct MCListTitleRow: View {var title: Stringvar body: some View {GeometryReader { geo inText(title).font(.title).bold().frame(width: UIScreen.main.bounds.width, height: 60).background(Color.black).foregroundColor(.white).SetPreference(geo: geo, save: true)}.frame(width: UIScreen.main.bounds.width, height: 60)}
}
/// 获得子视图 列表标题栏的大小
/// 将perference定义为modifier模式
struct setPreference: ViewModifier {var geo: GeometryProxyvar save: Boolfunc body(content: Content) -> some View {if save {content.preference(key: ListRowPreferenceKey.self,value:[ ListRowPreferenceData(rect: geo.frame(in: .named("VSTACK"))) ])} else {content}}
}extension View {func SetPreference(geo: GeometryProxy, save: Bool) -> some View {self.modifier(setPreference(geo: geo, save: save))}
}
在标题栏视图中,使用GeometryReader,配合在主视图中定义的"VSTACK"区域,来确定标题栏的尺寸,并“记录”在ListRowPreferenceKey中。
列表内容视图
struct MultiColumnListData: Identifiable, Hashable, Codable {var id = UUID()var column: Int = 1var widths: [CGFloat] = []var rowData: [String] = []
}// MARK: 列表内容行
struct MCListRow: View {var data: MultiColumnListData = MultiColumnListData()init(data: MultiColumnListData) {self.data = dataself.data.column = self.data.rowData.count == 0 ? 1 : self.data.rowData.countif self.data.widths.count == 0 {let widthAverage = UIScreen.main.bounds.width / CGFloat(self.data.column)for _ in 0..<self.data.column {self.data.widths.append(widthAverage)}}}var body: some View {HStack(spacing: 0) {ForEach(0..<data.column) { i inText(data.rowData[i]).frame(width: data.widths[i], height: 50).background(((i % 2) == 0) ? Color.white : Color.gray.opacity(0.3))}}.frame(height: 50)}
}
根据传入的rowData,来判断分为几列显示,默认按照屏幕宽度平均等分单元格的宽度
主视图关键代码如下:
VStack {...ScrollView {}}.coordinateSpace(name: "VSTACK").onPreferenceChange(ListRowPreferenceKey.self) { preferences in// 获得第一行标题栏中心点的Y坐标值let fixedMidY = preferences[0].rect.midYlet sequence = (fixedMidY - offset...fixedMidY + offset)for i in 1..<preferences.count {// 判断其他标题栏是否穿过第一行标题栏的位置if sequence.contains(preferences[i].rect.midY) {// 更新第一行标题栏显示if self.direction {// 向上滚动情况DispatchQueue.main.async {self.indexHeader = i}} else {// 向下滚动情况DispatchQueue.main.async {self.indexHeader = i - 1 < 0 ? 0 : i - 1}}}}}
在onPreferenceChange中,获取子视图我们设置到preference中的尺寸数据,并进行判断
完整代码如下:
MCListRow.swift
//
// MCListRow.swift
// MulitiColumnListTesting
//
// Created by Chr1s on 2021/8/29.
//import SwiftUIstruct ListRowPreferenceKey: PreferenceKey {typealias Value = [ListRowPreferenceData]static var defaultValue: [ListRowPreferenceData] = []static func reduce(value: inout [ListRowPreferenceData], nextValue: () -> [ListRowPreferenceData]) {value.append(contentsOf: nextValue())}
}struct ListRowPreferenceData: Equatable {let rect: CGRect
}// MARK: 列表标题行
struct MCListTitleRow: View {var title: Stringvar body: some View {GeometryReader { geo inText(title).font(.title).bold().frame(width: UIScreen.main.bounds.width, height: 60).background(Color.black).foregroundColor(.white).SetPreference(geo: geo, save: true)}.frame(width: UIScreen.main.bounds.width, height: 60)}
}// MARK: 列表内容行
struct MCListRow: View {var data: MultiColumnListData = MultiColumnListData()init(data: MultiColumnListData) {self.data = dataself.data.column = self.data.rowData.count == 0 ? 1 : self.data.rowData.countif self.data.widths.count == 0 {let widthAverage = UIScreen.main.bounds.width / CGFloat(self.data.column)for _ in 0..<self.data.column {self.data.widths.append(widthAverage)}}}var body: some View {HStack(spacing: 0) {ForEach(0..<data.column) { i inText(data.rowData[i]).frame(width: data.widths[i], height: 50).background(((i % 2) == 0) ? Color.white : Color.gray.opacity(0.3))}}.frame(height: 50)}
}/// 获得子视图 列表标题栏的大小
/// 将perference定义为modifier模式
struct setPreference: ViewModifier {var geo: GeometryProxyvar save: Boolfunc body(content: Content) -> some View {if save {content.preference(key: ListRowPreferenceKey.self,value:[ ListRowPreferenceData(rect: geo.frame(in: .named("VSTACK"))) ])} else {content}}
}extension View {func SetPreference(geo: GeometryProxy, save: Bool) -> some View {self.modifier(setPreference(geo: geo, save: save))}
}struct MCListRow_Previews: PreviewProvider {static var previews: some View {Group {MCListRow(data: MultiColumnListData(rowData: ["1","Lex","24"])).previewLayout(.sizeThatFits)MCListTitleRow(title: "前锋").previewLayout(.sizeThatFits).preferredColorScheme(.dark)
}}
}
MCListViewModel.swift
//
// MultiColumnListViewModel.swift
// MulitiColumnListTesting
//
// Created by Chr1s on 2021/8/30.
//import SwiftUIstruct MultiColumnListData: Identifiable, Hashable, Codable {var id = UUID()var column: Int = 1var widths: [CGFloat] = []var rowData: [String] = []
}class MultiColumnListViewModel: ObservableObject {@Published var listData: [MultiColumnListData] = []@Published var titleData: [String] = ["前锋", "中场", "后卫"]@Published var listColumn: Int = 1init() {listData.append(MultiColumnListData(rowData: ["7号", "格列兹曼", "30岁"]))listData.append(MultiColumnListData(rowData: ["9号", "德佩", "24岁"]))listData.append(MultiColumnListData(rowData: ["11号", "登贝莱", "24岁"]))listData.append(MultiColumnListData(rowData: ["7号", "格列兹曼", "30岁"]))listData.append(MultiColumnListData(rowData: ["9号", "德佩", "24岁"]))listData.append(MultiColumnListData(rowData: ["11号", "登贝莱", "24岁"]))listData.append(MultiColumnListData(rowData: ["5号", "布斯克茨", "33岁"]))listData.append(MultiColumnListData(rowData: ["21号", "德容", "22岁"]))listData.append(MultiColumnListData(rowData: ["20号", "比达尔", "30岁"]))listData.append(MultiColumnListData(rowData: ["8号", "皮亚尼奇", "25岁"]))listData.append(MultiColumnListData(rowData: ["5号", "布斯克茨", "33岁"]))listData.append(MultiColumnListData(rowData: ["21号", "德容", "22岁"]))listData.append(MultiColumnListData(rowData: ["20号", "比达尔", "30岁"]))listData.append(MultiColumnListData(rowData: ["8号", "皮亚尼奇", "25岁"]))listData.append(MultiColumnListData(rowData: ["3号", "皮克", "33岁"]))listData.append(MultiColumnListData(rowData: ["18号", "阿尔巴", "30岁"]))listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))listData.append(MultiColumnListData(rowData: ["3号", "皮克", "33岁"]))listData.append(MultiColumnListData(rowData: ["18号", "阿尔巴", "30岁"]))listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))listData.append(MultiColumnListData(rowData: ["3号", "皮克", "33岁"]))listData.append(MultiColumnListData(rowData: ["18号", "阿尔巴", "30岁"]))listData.append(MultiColumnListData(rowData: ["24号", "埃里克加西", "30岁"]))listData.append(MultiColumnListData(rowData: ["23号", "乌姆蒂蒂", "25岁"]))}
}
MCListView.swift
//
// MultiColumnList.swift
// MulitiColumnListTesting
//
// Created by Chr1s on 2021/8/29.
//import SwiftUIstruct MultiColumnList: View {@StateObject var vm = MultiColumnListViewModel()@State var indexHeader: Int = 0@State var direction: Bool = falselet offset: CGFloat = 5.0var body: some View {NavigationView {VStack(spacing: 0.0) {/// 固定的第一行列表,显示第一个TitleMCListTitleRow(title: vm.titleData[self.indexHeader])/// 滚动列表ScrollView(/*@START_MENU_TOKEN@*/.vertical/*@END_MENU_TOKEN@*/, showsIndicators: false) {VStack(spacing: 0) {ForEach(1..<7) { i inMCListRow(data: vm.listData[i])Divider()}MCListTitleRow(title: vm.titleData[1])ForEach(7..<15) { i inMCListRow(data: vm.listData[i])Divider()}MCListTitleRow(title: vm.titleData[2])ForEach(15..<vm.listData.count) { i inMCListRow(data: vm.listData[i])Divider()}}}.gesture(DragGesture().onChanged { value inif value.translation.height > 0 {self.direction = false} else {self.direction = true}})}.navigationTitle("多列列表").coordinateSpace(name: "VSTACK").onPreferenceChange(ListRowPreferenceKey.self) { preferences in// 获得第一行标题栏中心点的Y坐标值let fixedMidY = preferences[0].rect.midYlet sequence = (fixedMidY - offset...fixedMidY + offset)for i in 1..<preferences.count {// 判断其他标题栏是否穿过第一行标题栏的位置if sequence.contains(preferences[i].rect.midY) {// 更新第一行标题栏显示if self.direction {// 向上滚动情况DispatchQueue.main.async {self.indexHeader = i}} else {// 向下滚动情况DispatchQueue.main.async {self.indexHeader = i - 1 < 0 ? 0 : i - 1}}}}}}.onAppear {self.indexHeader = 0}}
}struct MultiColumnList_Previews: PreviewProvider {static var previews: some View {MultiColumnList()}
}
SwiftUI的多列列表相关推荐
- sqlserver 插入数据时异常,仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'XXXXX.dbo.XXXXXXXXX'中的标识列指定显式值。...
INSERT INTO XXXXXXXXX.dbo.XXXXXXXXX select * from XXXXXXXXX 仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'X ...
- LabVIEW多列列表框背景颜色操作
多列列表框 概述 在很多情况下我们需要在表格中某一列查找一些指定的元素,并且想让其显示为指定的颜色,此时就可以利用多列列表框的一些属性来解决这一问题. 知识点讲解 1. 在多列列表框中显示数据 1) ...
- bootstraptable列宽自适应内容_多列列表框行高和列宽的自适应调整
LabVIEW:2015 在使用多列列表框(Multicolumn Listbox)时,有时不同列显示的内容长度不一致,为了显示更加美观,并节省界面显示空间,需要实现多列列表框的行高和列宽的自适应调整 ...
- 仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'Address'中的标识列指
仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'Address'中的标识列指 解决办法,将主键的ID自增长先设置为false 转载于:https://www.cnblog ...
- 自学IOS开发第3天·基础SwiftUI之动态滑动列表(上)
文章目录 基础SwiftUI之动态滑动列表 UI构建 创建模型 BlogerData.swift 创建 JSON文件 创建 Model.swift 附稿 基础SwiftUI之动态滑动列表 我完全跟着S ...
- LabVIEW多列列表框简单玩法
多列列表框简单玩法 1.场景: 表格显示数据太单调?!要想做到像Excel表格或者其他APP那样丰富多彩怎么办?告诉你个秘密,其实可以通过多列列表框来实现哦! 话不多说直接接上内容 2.环境 Wind ...
- 多列列表控件中图片尺寸处理的若干问题
一.自适应 当前终端设备种类繁多,即单以 iOS 设备论计,从 iPhone 3GS 到 iPhone 6 Plus 总的数量亦不少了.如果对图片的什么尺寸都不指定(默认),或者只确定一种尺寸(也就是 ...
- 消息 8101,级别 16,状态 1,第 1 行仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'CUSTOMER_TBL'中的标识列指定显式值。...
像这样的问题怎么解决呢? 问题分析: 意思是你的主键是自动编号类型的,所以不能向该列插入数据. 解决办法: 执行 语句 :SET IDENTITY_INSERT CUSTOMER_TBL ON 然后在 ...
- python每行乘列表_python – 在pandas数据帧中查找每行的两列列表中哪一列的最快方法...
我正在寻找最快的方法来做到以下几点: 我们有一个pd.DataFrame: df = pd.DataFrame({ 'High': [1.3,1.2,1.1], 'Low': [1.3,1.2,1.1 ...
最新文章
- RMSD:通过旋转计算两个分子间的最小rmsd
- 解决CPC撰写文档报错问题“无法获取“AxforApplication”控件的窗口句柄。不支持无窗口的 ActiveX 控件”
- 脑科学与脑电基础知识汇总
- 初学Java Web(5)——cookie-session学习
- C# Combobox联动
- python取消任务的方法_python-即使忽略CancelledError,如何取消任务执...
- 订阅基础:RSS、ATOM、FEED、聚合、供稿、合烧与订阅
- OpenSSL--Window生成证书实战
- 新工科背景下大数据专业导论课程的改革与探索
- 进程之间信号收发并携带数据
- Alex 的 Hadoop 菜鸟教程: 第7课 Hbase 使用教程
- CodeForces703D Mishka and Interesting sum(树状数组)
- 授权公众号第三方平台和开发者模式冲突吗?
- 格美家具网站项目总结
- Viso跨职能流程图连接点操作
- CSS实现进度条和订单进度条
- 优雅的进行线上数据订正
- 大型游戏行业网络技术解决方案
- excel表格下拉选项怎么设置_让表格美观好看几个Excel设置技巧
- 1166:求f(x,n)
热门文章
- python——利用记忆曲线制作单词计划表
- 计算机毕业设计-基于微信小程序高校学生课堂扫码考勤签到系统-校园考勤打卡签到小程序
- php excel 右对齐,excel中单元格对齐方式在哪里设置?
- echarts图表x轴基准线(平行y轴)
- 前端实现实时消息提醒消息通知
- 解题:BZOJ 2673 World Final 2011 Chips Challenge
- matlab 图像白平衡算法,Matlab常用白平衡算法
- android显示地图代码,Android Studio之高德地图实现定位和3D地图显示(示例代码)
- 安卓系统 samba服务器搭建,介绍在电脑上建samba服务器的方法 安卓samba服务器的安装方法...
- 信贷反欺诈体系介绍及其策略规则应用