There are two ways you could solve this. First, you could use two LazyVGrids with the same column set up, but you would have to fix some of your sizes. The second way isYou can solve this with with PreferenceKeys.
LazyVGrid:
struct LazyVGridView: View {
let columns = [
GridItem(.fixed(100)),
GridItem(.fixed(100)),
GridItem(.adaptive(minimum: 180))
]
var body: some View {
VStack {
LazyVGrid(columns: columns) {
Text("Name")
.padding()
Text("Balance")
.padding()
Text("Currency")
.padding()
}
.foregroundColor(Color(#colorLiteral(red: 0.4783782363, green: 0.4784628153, blue: 0.4783664346, alpha: 1)))
.background(Color(#colorLiteral(red: 0.9332349896, green: 0.9333916306, blue: 0.9332130551, alpha: 1)))
LazyVGrid(columns: columns) {
ForEach(clients) { client in
Text(client.name)
.padding()
Text(client.balance.description)
.padding()
Text(client.currency)
.padding()
}
}
}
}
}
`PreferenceKeys`:
struct PrefKeyAlignmentView: View {
let clients: [ClientInfo] = [
ClientInfo(name: "John", balance: 300, currency: "EUR"),
ClientInfo(name: "Jennifer", balance: 53000, currency: "EUR"),
ClientInfo(name: "Jo", balance: 30, currency: "EUR"),
ClientInfo(name: "Jill", balance: 530000, currency: "EUR")
]
@State private var col1Width: CGFloat = 100
@State private var col2Width: CGFloat = 100
@State private var col3Width: CGFloat = 110
var body: some View {
VStack {
HStack() {
Text("Name")
.padding()
// The background view takes on the size of the Text + the padding, and the GeometryReader reads that size
.background(GeometryReader { geometry in
Color.clear.preference(
//This sets the preference key value with the width of the background view
key: Col1WidthPrefKey.self,
value: geometry.size.width)
})
// This set the frame to the column width variable defined above
.frame(width: col1Width)
Text("Balance")
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col2WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col2Width)
Text("Currency")
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col3WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col3Width)
Spacer()
}
ForEach(clients) { client in
HStack {
Text(client.name)
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col1WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col1Width)
Text(client.balance.description)
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col2WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col2Width)
Text(client.currency)
.padding()
.background(GeometryReader { geometry in
Color.clear.preference(
key: Col3WidthPrefKey.self,
value: geometry.size.width)
})
.frame(width: col3Width)
Spacer()
}
}
}
//This sets the various column widths whenever a new entry as found.
//It keeps the larger of the new item's width, or the last largest width.
.onPreferenceChange(Col1WidthPrefKey.self) {
col1Width = max($0,col1Width)
}
.onPreferenceChange(Col2WidthPrefKey.self) {
col2Width = max($0,col2Width)
}
.onPreferenceChange(Col3WidthPrefKey.self) {
col3Width = max($0,col3Width)
}
}
}
struct ClientInfo: Identifiable {
let id = UUID()
let name: String
let balance: Int
let currency: String
}
private extension PrefKeyAlignmentView {
struct Col1WidthPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct Col2WidthPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
struct Col3WidthPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
}
I don't think using alignmentGuides would work as you would have to handle multiple views, and alignment guides only align one set of views, and not the views internally.
Both of these solutions will work. The PreferenceKeys will automatically adapt everything, but theoretically could push past the bounds of the screen with too much information. The LazyVGrid is more rigid, but could cause truncation/wrapping if the data is too long. I don't think you could use an adaptive layout for the LazyVGrid for all columns, if you have two different grids. It may be possible to mix both and set the LazyVGrid widths with PreferenceKeys, but I haven't personally tried it.
DataSource for both for testing:
struct ClientInfo: Identifiable {
let id = UUID()
let name: String
let balance: Int
let currency: String
}
let clients: [ClientInfo] = [
ClientInfo(name: "John", balance: 300, currency: "EUR"),
ClientInfo(name: "Jennifer", balance: 53000, currency: "EUR"),
ClientInfo(name: "Jo", balance: 30, currency: "EUR"),
ClientInfo(name: "Jill", balance: 530000, currency: "EUR")
]
Edit: While working on the answer, I forgot you were restricted to iOS 13. LazyVGrids aren't available. I left the LazyVGrid answer up as you could use if #available(os:) if you wanted to use it for later OS's, or for someone else looking for a more current solution.
Second Edit: I was thinking about how this view could be made more reusable, and I came up with a third way that is also iOS 13 compatible, and only uses one PreferenceKey to help set the background color on the headers. This also allows unlimited columns. I think this is the simplest way of the bunch:
struct TableView: View {
let headers: [String]
let tableData: [String:[String]]
let dataAlignment: [String:HorizontalAlignment]
let headerColor: Color
@State var headerHeight: CGFloat = 30
init(headers: [String], headerColor: Color = Color.clear, columnData: [[String]], alignment: [HorizontalAlignment]) {
self.headers = headers
self.headerColor = headerColor
if headers.count == columnData.count,
headers.count == alignment.count {
var data: [String:[String]] = [:]
var align: [String:HorizontalAlignment] = [:]
for i in 0..<headers.count {
let key = headers[i]
let value = columnData[i]
data[key] = value
align[key] = alignment[i]
}
dataAlignment = align
tableData = data
} else {
tableData = [:]
dataAlignment = [:]
}
}
var body: some View {
ScrollView {
HStack(spacing: 30) {
ForEach(headers, id: \.self) { header in
VStack {
Text(header)
.padding()
.background( GeometryReader { geometry in
Color.clear.preference(
key: HeaderHeightPrefKey.self,
value: geometry.size.height)
})
VStack(alignment: dataAlignment[header] ?? .center) {
ForEach(tableData[header] ?? [], id: \.self) { entry in
Text(entry)
}
}
}
}
}
.background(
VStack {
Rectangle()
.fill(headerColor)
.frame(height: headerHeight, alignment: .top)
Spacer()
}
)
.onPreferenceChange(HeaderHeightPrefKey.self) {
headerHeight = max($0,headerHeight)
}
}
}
}
private extension TableView {
struct HeaderHeightPrefKey: PreferenceKey {
static let defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat,
nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
}
And it is used like this:
TableView(headers: ["Name", "Balance", "Currency"], headerColor: Color(uiColor: .systemGray5), columnData: [
["John", "Jennifer", "Jo", "Jill"],
["300", "53000", "30", "530000"],
["EUR", "EUR", "EUR", "EUR"]
], alignment: [.center, .trailing, .center])
and produces this:
