源代码: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的多列列表相关推荐

  1. sqlserver 插入数据时异常,仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'XXXXX.dbo.XXXXXXXXX'中的标识列指定显式值。...

    INSERT INTO XXXXXXXXX.dbo.XXXXXXXXX select * from XXXXXXXXX 仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'X ...

  2. LabVIEW多列列表框背景颜色操作

    多列列表框 概述 在很多情况下我们需要在表格中某一列查找一些指定的元素,并且想让其显示为指定的颜色,此时就可以利用多列列表框的一些属性来解决这一问题. 知识点讲解 1. 在多列列表框中显示数据 1) ...

  3. bootstraptable列宽自适应内容_多列列表框行高和列宽的自适应调整

    LabVIEW:2015 在使用多列列表框(Multicolumn Listbox)时,有时不同列显示的内容长度不一致,为了显示更加美观,并节省界面显示空间,需要实现多列列表框的行高和列宽的自适应调整 ...

  4. 仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'Address'中的标识列指

    仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'Address'中的标识列指 解决办法,将主键的ID自增长先设置为false 转载于:https://www.cnblog ...

  5. 自学IOS开发第3天·基础SwiftUI之动态滑动列表(上)

    文章目录 基础SwiftUI之动态滑动列表 UI构建 创建模型 BlogerData.swift 创建 JSON文件 创建 Model.swift 附稿 基础SwiftUI之动态滑动列表 我完全跟着S ...

  6. LabVIEW多列列表框简单玩法

    多列列表框简单玩法 1.场景: 表格显示数据太单调?!要想做到像Excel表格或者其他APP那样丰富多彩怎么办?告诉你个秘密,其实可以通过多列列表框来实现哦! 话不多说直接接上内容 2.环境 Wind ...

  7. 多列列表控件中图片尺寸处理的若干问题

    一.自适应 当前终端设备种类繁多,即单以 iOS 设备论计,从 iPhone 3GS 到 iPhone 6 Plus 总的数量亦不少了.如果对图片的什么尺寸都不指定(默认),或者只确定一种尺寸(也就是 ...

  8. 消息 8101,级别 16,状态 1,第 1 行仅当使用了列列表并且 IDENTITY_INSERT 为 ON 时,才能为表'CUSTOMER_TBL'中的标识列指定显式值。...

    像这样的问题怎么解决呢? 问题分析: 意思是你的主键是自动编号类型的,所以不能向该列插入数据. 解决办法: 执行 语句 :SET IDENTITY_INSERT CUSTOMER_TBL ON 然后在 ...

  9. python每行乘列表_python – 在pandas数据帧中查找每行的两列列表中哪一列的最快方法...

    我正在寻找最快的方法来做到以下几点: 我们有一个pd.DataFrame: df = pd.DataFrame({ 'High': [1.3,1.2,1.1], 'Low': [1.3,1.2,1.1 ...

最新文章

  1. RMSD:通过旋转计算两个分子间的最小rmsd
  2. 解决CPC撰写文档报错问题“无法获取“AxforApplication”控件的窗口句柄。不支持无窗口的 ActiveX 控件”
  3. 脑科学与脑电基础知识汇总
  4. 初学Java Web(5)——cookie-session学习
  5. C# Combobox联动
  6. python取消任务的方法_python-即使忽略CancelledError,如何取消任务执...
  7. 订阅基础:RSS、ATOM、FEED、聚合、供稿、合烧与订阅
  8. OpenSSL--Window生成证书实战
  9. 新工科背景下大数据专业导论课程的改革与探索
  10. 进程之间信号收发并携带数据
  11. Alex 的 Hadoop 菜鸟教程: 第7课 Hbase 使用教程
  12. CodeForces703D Mishka and Interesting sum(树状数组)
  13. 授权公众号第三方平台和开发者模式冲突吗?
  14. 格美家具网站项目总结
  15. Viso跨职能流程图连接点操作
  16. CSS实现进度条和订单进度条
  17. 优雅的进行线上数据订正
  18. 大型游戏行业网络技术解决方案
  19. excel表格下拉选项怎么设置_让表格美观好看几个Excel设置技巧
  20. 1166:求f(x,n)

热门文章

  1. python——利用记忆曲线制作单词计划表
  2. 计算机毕业设计-基于微信小程序高校学生课堂扫码考勤签到系统-校园考勤打卡签到小程序
  3. php excel 右对齐,excel中单元格对齐方式在哪里设置?
  4. echarts图表x轴基准线(平行y轴)
  5. 前端实现实时消息提醒消息通知
  6. 解题:BZOJ 2673 World Final 2011 Chips Challenge
  7. matlab 图像白平衡算法,Matlab常用白平衡算法
  8. android显示地图代码,Android Studio之高德地图实现定位和3D地图显示(示例代码)
  9. 安卓系统 samba服务器搭建,介绍在电脑上建samba服务器的方法 安卓samba服务器的安装方法...
  10. 信贷反欺诈体系介绍及其策略规则应用