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:
- Remove the header and footer rows and show these outside the
Grid.
- The
Grid itself can then be wrapped in a ScrollView.
- To align the header and footer items, the widths of the columns can be measured by attaching an
.onGeometryChange modifier to each cell.
- The maximum width of the cells in a column is stored in an array.
- If you are expecting that any of the title or footer items will have the greatest width then you could use the technique in reverse and set the width of a column from the width of a title or footer item.
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
}
}
}
}
}

Griddoes not support this. How about aVStackwithScrollView(.horizontal) { HStack {...} }inside it?