源代码: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: String
var body: some View {
GeometryReader { geo in
Text(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: GeometryProxy
var save: Bool
func 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 = 1
var widths: [CGFloat] = []
var rowData: [String] = []
}
// MARK: 列表内容行
stru