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
}
}
我使用 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
}
}