I have SwiftUI grid of 25 rows with a first header row and a totals row at the bottom. I would like the header and the totals row to remain fixed, but allow the user to scroll the 23 rows (as they often do not fit the screen). Using ScrollView on certain GridRows does not work...
import SwiftUI
struct ContentView: View {
var body: some View {
Grid {
GridRow {
Text("Name")
Text("Age")
}
.font(.headline)
// would like to make the following 'scrollable'
GridRow {
Text("Marc")
Text("25")
}
GridRow {
Text("Francis")
Text("36")
}
// ... many more GridRows ...
// end of scrolling
GridRow {
Text("Total Age:")
Text("61")
}
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I assume you want the scrollable rows to scroll vertically, right?
Some possible approaches:
An easy solution would be to use fixed widths for the columns. Then the grid can simply be built using a VStack
with rows of HStack
. Or, instead of HStack
for the rows, you could consider using a custom Layout
that uses percentages or weights for the column widths. An example of such a layout can be found in the answer to SwiftUI How to Set Specific Width Ratios for Child Elements in an HStack (it was my answer).
If you were to use a LazyVGrid
then you could put the titles in the header
and the totals in the footer
and pin the header and footer. But that still leaves the issue of alignment.
You could also consider using a Table
, which automatically adds scrolling. However, when I tried it on iOS, it seems the column headers are automatically hidden if scrolling is needed and there is no way to force them to be shown. Even applying .tableColumnHeaders(.visible)
doesn't help. Also, I don't think there is any concept of having a footer row at the end. So for iOS, a Table
is probably not useful.
Here is another approach that keeps it all dynamic:
Grid
.Grid
itself can then be wrapped in a ScrollView
..onGeometryChange
modifier to each cell.Here is an example to show it working. The width of the first column (Name) is determined by the grid contents, the width of the second column (Age) is determined by the title.
struct ContentView: View {
private let spacing: CGFloat = 10
@State private var colWidths = [CGFloat](repeating: 0, count: 2)
var body: some View {
VStack {
HStack(spacing: spacing) {
Text("Name")
.frame(width: colWidths[0], alignment: .leading)
Text("Age")
.modifier(WidthRecorder(maxWidth: $colWidths[1]))
}
.font(.headline)
ScrollView {
Grid(alignment: .leading, horizontalSpacing: spacing) {
GridRow {
Text("Marc")
.modifier(WidthRecorder(maxWidth: $colWidths[0]))
Text("25")
.frame(width: colWidths[1], alignment: .leading)
}
GridRow {
Text("Francis")
.modifier(WidthRecorder(maxWidth: $colWidths[0]))
Text("36")
.frame(width: colWidths[1], alignment: .leading)
}
ForEach(1..<50) { i in
GridRow {
Text("Another row \(i)")
.modifier(WidthRecorder(maxWidth: $colWidths[0]))
Text("\(i)")
.frame(width: colWidths[1], alignment: .leading)
}
}
}
}
HStack(spacing: spacing) {
Text("Total Age:")
.frame(width: colWidths[0], alignment: .leading)
Text("61")
.frame(width: colWidths[1], alignment: .leading)
}
}
}
struct WidthRecorder: ViewModifier {
@Binding var maxWidth: CGFloat
func body(content: Content) -> some View {
content
.onGeometryChange(for: CGFloat.self) { proxy in
proxy.size.width
} action: { newVal in
if newVal > maxWidth {
maxWidth = newVal
}
}
}
}
}