You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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) {}
}

修改说明

  1. 消除左上角空白:将左侧VStack顶部的Color.clear替换为与行列头样式一致的单元格,填补空白区域,使整体布局对齐
  2. 消除行头与内容间隙:给所有ScrollView添加.padding(.top, 0)移除默认内边距,同时修正垂直偏移的取值(max(value.y, 0)),避免行头因过度偏移产生间隙
  3. 保持行列头固定逻辑:保留原有的滚动偏移同步逻辑,确保行头随垂直滚动固定、列头随水平滚动固定

内容的提问来源于stack exchange,提问作者user1664018

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 22:56:04