SwiftUI固定行列头可滚动表格:行列头调整问题求助
Excel样式滚动视图的行列头优化问题
基于StackOverflow上关于「SwiftUI实现Excel样式滚动视图并固定表头」的问答,现提出后续优化需求:
- 将每行首个值设为行头,水平滚动时保持固定
- 将列头左移一个单元格,消除左上角空白区域
目前已实现首列固定为行头,但存在两个问题:列头无法左移导致左上角留有空白,且行头与行内容之间存在间隙。以下是问题代码及效果截图:
问题代码
import Foundation import SwiftUI struct ContentView: View { @State private var offset = CGPoint.zero @State private var colsHeaders: [String] = [] @State private var rowData: [[String]] = [] var body: some View { HStack(alignment: .top, spacing: 0) { VStack(alignment: .leading, spacing: 0) { Color.clear.frame(width: 150, height: 50) ScrollView([.vertical]) { rowsHeader .offset(y: offset.y) } .disabled(true) .scrollIndicators(.hidden) } VStack(alignment: .leading, spacing: 0) { ScrollView([.horizontal]) { colsHeader .offset(x: offset.x) } .disabled(true) table .coordinateSpace(name: "scroll") } } .padding() .onAppear { readJSONFromFile() } } var colsHeader: some View { HStack(alignment: .top, spacing: 0) { ForEach(colsHeaders, id: \.self) { header in Text(header) .foregroundColor(.primary) .frame(width: 150, height: 50) .border(Color.gray) } } } var rowsHeader: some View { VStack(alignment: .leading, spacing: 0) { ForEach(0..<rowData.count, id: \.self) { row in Text(rowData[row].first ?? "N/A") .foregroundColor(.primary) .frame(width: 150, height: 50) .border(Color.gray) } } } var table: some View { ScrollView([.vertical, .horizontal]) { VStack(alignment: .leading, spacing: 0) { ForEach(0..<rowData.count, id: \.self) { row in HStack(alignment: .top, spacing: 0) { ForEach(1..<rowData[row].count, id: \.self) { col in Text(rowData[row][col]) .frame(width: 150, height: 50) .border(Color.gray) .id("\(row)_\(col)") } } } } .background( GeometryReader { geo in Color.clear .preference(key: ViewOffsetKey.self, value: geo.frame(in: .named("scroll")).origin) }) .onPreferenceChange(ViewOffsetKey.self) { value in offset = value } } } } // 补充缺失的ViewOffsetKey定义 struct ViewOffsetKey: PreferenceKey { static var defaultValue: CGPoint = .zero static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {} }
问题效果截图

解决方案代码
import Foundation import SwiftUI struct ContentView: View { @State private var offset = CGPoint.zero @State private var colsHeaders: [String] = [] @State private var rowData: [[String]] = [] var body: some View { HStack(alignment: .top, spacing: 0) { // 行头区域(含左上角交叉单元格) VStack(alignment: .leading, spacing: 0) { // 替换空白为统一样式的交叉单元格 Text("") .foregroundColor(.primary) .frame(width: 150, height: 50) .border(Color.gray) ScrollView([.vertical]) { rowsHeader .offset(y: offset.y) } .disabled(true) .scrollIndicators(.hidden) .padding(.top, 0) // 移除不必要的内边距 } // 列头+表格区域 VStack(alignment: .leading, spacing: 0) { ScrollView([.horizontal]) { colsHeader .offset(x: offset.x) } .disabled(true) .padding(.top, 0) // 移除不必要的内边距 table .coordinateSpace(name: "scroll") } } .padding() .onAppear { readJSONFromFile() } } var colsHeader: some View { HStack(alignment: .top, spacing: 0) { ForEach(colsHeaders, id: \.self) { header in Text(header) .foregroundColor(.primary) .frame(width: 150, height: 50) .border(Color.gray) } } } var rowsHeader: some View { VStack(alignment: .leading, spacing: 0) { ForEach(0..<rowData.count, id: \.self) { row in Text(rowData[row].first ?? "N/A") .foregroundColor(.primary) .frame(width: 150, height: 50) .border(Color.gray) } } } var table: some View { ScrollView([.vertical, .horizontal]) { VStack(alignment: .leading, spacing: 0) { ForEach(0..<rowData.count, id: \.self) { row in HStack(alignment: .top, spacing: 0) { ForEach(1..<rowData[row].count, id: \.self) { col in Text(rowData[row][col]) .frame(width: 150, height: 50) .border(Color.gray) .id("\(row)_\(col)") } } } } .background( GeometryReader { geo in Color.clear .preference(key: ViewOffsetKey.self, value: geo.frame(in: .named("scroll")).origin) }) .onPreferenceChange(ViewOffsetKey.self) { value in // 修正垂直偏移,避免行头与内容错位 offset = CGPoint(x: value.x, y: max(value.y, 0)) } } .padding(.top, 0) // 移除不必要的内边距 } } struct ViewOffsetKey: PreferenceKey { static var defaultValue: CGPoint = .zero static func reduce(value: inout CGPoint, nextValue: () -> CGPoint) {} }
修改说明
- 消除左上角空白:将左侧VStack顶部的
Color.clear替换为与行列头样式一致的单元格,填补空白区域,使整体布局对齐 - 消除行头与内容间隙:给所有ScrollView添加
.padding(.top, 0)移除默认内边距,同时修正垂直偏移的取值(max(value.y, 0)),避免行头因过度偏移产生间隙 - 保持行列头固定逻辑:保留原有的滚动偏移同步逻辑,确保行头随垂直滚动固定、列头随水平滚动固定
内容的提问来源于stack exchange,提问作者user1664018
相关产品推荐
相关产品推荐

