swiftuigridswiftui-list

What is the correct way to get a column to take all available space in a grid?


This should be a simple thing I am missing so I apologize in advance. I have reusable view "TextRowView" that has 2 grid columns one will be a fixed width and the other I would like to take up all available space.

I don't understand why It displays correctly in the TruckView Preview but doesn't display correctly in the list view

struct TextRowView: View {
    
    let columns = [
        GridItem(.fixed(140), alignment: .leading),
        GridItem(.flexible(), alignment: .leading)
    ]
    
    let title: String
    let value: String
    
    var body: some View {
        LazyVGrid(columns: columns) {
            Text(title)
                .bold()
            
            Text(value)
        }
    }
}

Here is the cell for the list view

import SwiftUI

struct TruckView: View {
    
    let truck: Truck
    
    var body: some View {
        VStack {
            TextRowView(title: "Year:", value: truck.year)
            
            TextRowView(title: "Make:", value: truck.make)
            
            TextRowView(title: "Model:", value: truck.model)
            
            TextRowView(title: "Driver:", value: truck.driver)
            
            TextRowView(title: "Truck #:", value: truck.truckNumber.formatted(.number.grouping(.never)))
        }
        .foregroundStyle(Color(hex: truck.foregroundColor))
        .padding()
        .background(RoundedRectangle(cornerRadius: 10)
            .fill(Color(hex: truck.backgroundColor)))
    }
}

here is the list view

import SwiftUI

struct TripListView: View {
    
    @Environment(AppController.self) private var appController
    
    var body: some View {
        Group {
            List {
                ForEach(appController.filteredTrucks) { truck in
                    NavigationLink {
                        
                    } label: {
                        TruckView(truck: truck)
                    }
                }
            }
        }
    }
}

Here is what it looks like in the simulator.

TruckListView Screenshot

and this is the TruckView in the preview.

TruckView

This appears to work but I am not sure if it is the correct / best way of handling this situation.

struct TextRowView: View {
    
    let columns = [
        GridItem(.fixed(140), alignment: .leading),
        GridItem(.flexible(minimum: UIScreen.main.bounds.width - 210), alignment: .leading)
    ]
    
    let title: String
    let value: String
    
    var body: some View {
        LazyVGrid(columns: columns) {
            Text(title)
                .bold()
            
            Text(value)
        }
    }
}

UPDATE: I was using this and was told a Grid would be better.

struct TextRowView: View {
    let title: String
    let value: String
    let width: CGFloat
    
    var body: some View {
        ViewThatFits(in: .horizontal) {
            LabeledContent {
                Text(value)
                    .frame(width: width, alignment: .leading)
            } label: {
                Text(title)
                    .bold()
            }
            
            VStack {
                Text(title)
                    .bold()
                
                Text(value)
                    .frame(width: width, alignment: .leading)
            }
        }
    }
}

Solution

  • Since you are only displaying one row of details and the width of the first cell is fixed, it would be simpler to use an HStack in TextRowView instead:

    // TextRowView
    
    HStack {
        Text(title)
            .bold()
            .frame(maxWidth: 140, alignment: .leading)
    
        Text(value)
            .frame(maxWidth: .infinity, alignment: .leading)
    }
    

    Using a Grid only makes sense if there is more than one row. So if you really want to use a grid, I would suggest the following changes:

    1. In TextRowView, remove the parent container. This way, the body function returns two "loose" views. This works, because View.body is implicitly a ViewBuilder.

    2. Move the definition of columns from TextRowView to TruckView.

    3. In TruckView, change the VStack to LazyVGrid and supply the columns that came from TextRowView.

    4. Since the grid is shown in a List row, apply a frame with maxWidth: .infinity, so that it fills the full width of the row.

    Here are the updated versions of these two views:

    struct TextRowView: View {
        let title: String
        let value: String
        
        var body: some View {
            Text(title)
                .bold()
            Text(value)
        }
    }
    
    struct TruckView: View {
        let columns = [ // 👈 moved from TextRowView
            GridItem(.fixed(140), alignment: .leading),
            GridItem(.flexible(), alignment: .leading)
        ]
        let truck: Truck
    
        var body: some View {
            LazyVGrid(columns: columns) { // 👈 changed
                TextRowView(title: "Year:", value: truck.year)
    
                TextRowView(title: "Make:", value: truck.make)
    
                TextRowView(title: "Model:", value: truck.model)
    
                TextRowView(title: "Driver:", value: truck.driver)
    
                TextRowView(title: "Truck #:", value: truck.truckNumber.formatted(.number.grouping(.never)))
            }
            .frame(maxWidth: .infinity) // 👈 added
            .foregroundStyle(Color(hex: truck.foregroundColor))
            .padding()
            .background(RoundedRectangle(cornerRadius: 10)
                .fill(Color(hex: truck.backgroundColor)))
        }
    }
    

    Since TextRowView is now so trivial, you could replace it with a function in TruckView instead. The function needs to be annotated with @ViewBuilder:

    @ViewBuilder
    private func gridRow(title: String, value: String) -> some View {
        Text(title)
            .bold()
        Text(value)
    }
    
    LazyVGrid(columns: columns) {
        gridRow(title: "Year:", value: truck.year)
    
        // ... etc.
    }