SwiftUI:在滚动视图中固定 headers,它在 excel 中具有垂直和水平滚动,如视图

SwiftUI: Pin headers in scrollview which has vertical and horizontal scroll in excel like view

我使用 multi-directional 滚动视图创建了一个 excel-like 视图。现在我想固定 headers,不仅是 headers 列,还有 headers 行。请看下面的 gif:

我用来创建这个视图的代码:

        ScrollView([.vertical, .horizontal]){
            VStack(spacing: 0){
                ForEach(0..<model.rows.count+1, id: \.self) {rowIndex in
                    
                    HStack(spacing: 0) {
                        ForEach(0..<model.columns.count+1) { columnIndex in
                            
                            if rowIndex == 0 && columnIndex == 0 {
                                Rectangle()
                                    .fill(Color(UIColor(Color.white).withAlphaComponent(0.0)))
                                    .frame(width: CGFloat(200).pixelsToPoints(), height: CGFloat(100).pixelsToPoints())
                                    .padding([.leading, .trailing])
                                    .border(width: 1, edges: [.bottom, .trailing], color: .blue)
                            } else if (rowIndex == 0 && columnIndex > 0) {
                                TitleText(
                                    label: model.columns[columnIndex - 1].label,
                                    columnWidth: CGFloat(columnWidth).pixelsToPoints(),
                                    borderEgdes: [.top, .trailing, .bottom]
                                )
                            } else if (rowIndex > 0 && columnIndex == 0) {
                                TitleText(
                                    label: model.rows[rowIndex - 1].label,
                                    columnWidth: CGFloat(columnWidth).pixelsToPoints(),
                                    borderEgdes: [.trailing, .bottom, .leading]
                                )
                            } else if (rowIndex > 0){
                                //text boxes
                                let column = model.columns[columnIndex - 1]
                                switch column.type {
                                case "Text":
                                    MatrixTextField(keyboardType: .default)
                                case "Number":
                                    MatrixTextField(keyboardType: .decimalPad)
                                case "RadioButton":
                                    RadioButton()
                                case "Checkbox":
                                    MatrixCheckbox()
                                default:
                                    MatrixTextField(keyboardType: .default)
                                }
                                
                            }
                            
                        }
                    }
                    
                }
            }
        }
        .frame(maxHeight: 500)

是否可以在此处同时固定列和行 headers?

重要的是我只使用 VStack 和 HStack 而不是 LazyVStack 和 LazyHStack,因为我需要滚动时的平滑度,当我使用 Lazy 堆栈时,由于明显的原因它会抖动很多。所以不能在这里真正使用 headers 部分。

我还可以采用哪些其他方法?

比预期的要复杂一些。您必须使用 .preferenceKey 来对齐所有三个 ScollView。这是一个工作示例:

struct ContentView: View {
    
    let columns = 20
    let rows = 30
    
    @State private var offset = CGPoint.zero
    
    var body: some View {
        
        HStack(alignment: .top, spacing: 0) {
            
            VStack(alignment: .leading, spacing: 0) {
                // empty corner
                Color.clear.frame(width: 70, height: 50)
                ScrollView([.vertical]) {
                    rowsHeader
                        .offset(y: offset.y)
                }
                .disabled(true)
            }
            VStack(alignment: .leading, spacing: 0) {
                ScrollView([.horizontal]) {
                    colsHeader
                        .offset(x: offset.x)
                }
                .disabled(true)

                table
                    .coordinateSpace(name: "scroll")
            }
        }
        .padding()
    }
    
    var colsHeader: some View {
        HStack(alignment: .top, spacing: 0) {
            ForEach(0..<columns) { col in
                Text("COL \(col)")
                    .foregroundColor(.secondary)
                    .font(.caption)
                    .frame(width: 70, height: 50)
                    .border(Color.blue)
            }
        }
    }
    
    var rowsHeader: some View {
        VStack(alignment: .leading, spacing: 0) {
            ForEach(0..<rows) { row in
                Text("ROW \(row)")
                    .foregroundColor(.secondary)
                    .font(.caption)
                    .frame(width: 70, height: 50)
                    .border(Color.blue)
            }
        }
    }
    
    var table: some View {
        ScrollView([.vertical, .horizontal]) {
            VStack(alignment: .leading, spacing: 0) {
                ForEach(0..<rows) { row in
                    HStack(alignment: .top, spacing: 0) {
                        ForEach(0..<columns) { col in
                            // Cell
                            Text("(\(row), \(col))")
                                .frame(width: 70, height: 50)
                                .border(Color.blue)
                                .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
                print("offset >> \(value)")
                offset = value
            }
        }
    }
    
}

struct ViewOffsetKey: PreferenceKey {
    typealias Value = CGPoint
    static var defaultValue = CGPoint.zero
    static func reduce(value: inout Value, nextValue: () -> Value) {
        value.x += nextValue().x
        value.y += nextValue().y
    }
}