swiftswiftuilayoutgridlazyvgrid

SwiftUI LazyVGrid content compression


I am trying to build a grid, consisting of items with image and text under it. I want all images be the same size and the text to expand cell's height as much as text is needed. But Image is always compressed to preserve all cells equal height and the text not to be truncated.

I've tried a lot of modifiers for LazyVGrid and also setting size parameters for GridItem but still not reached desired result

What should I do to build described UI? Should I use LazyVGrid or it is better to use Grid (the content is static and items quantity is relatively small to think about optimization)?

enter image description here

    let layout = [
        GridItem(),
        GridItem(),
        GridItem()
    ]
    
    var body: some View {
        ScrollView {
            LazyVGrid(columns: layout, spacing: 10) {
                ForEach(cardsContainer.cards) { card in
                    VStack {
                        if let uiImage = UIImage(named: card.imageName) {
                            Image(uiImage: uiImage)
                                .resizable()
                                .scaledToFill()
                                .clipped()
                        }
                        Text(card.name)
                        Text(card.arcana.rawValue)
                    }
                }
            }
        }
    }

PS May be smbdy can advice some good resources about advanced aspects of collections building with SwiftUI cuz I find only basic info about it and only a little info with the comparison of Grids vs Stacks in SwiftUI.


Solution

  • It might help if you use change the alignment of the GridItem to .top and then use .scaledToFit instead of .scaledToFill on the images. Like this:

    let layout = [
        GridItem(alignment: .top),
        GridItem(alignment: .top),
        GridItem(alignment: .top)
    ]
    
    VStack {
        if let uiImage = UIImage(named: card.imageName) {
            Image(uiImage: uiImage)
                .resizable()
                .scaledToFit() // not .scaledToFill
                .clipped()
        }
        Text(card.name)
        Text(card.arcana.rawValue)
    }
    

    Otherwise, the way to fix the misaligned grid contents is to make sure the labels always occupy the same space. This is done by establishing a footprint for a standard label, then showing the actual label in an overlay.

    There would be two variations of this approach, depending on how you want to handle names that are too long for a single line.

    1. Allow the font to shrink

    You can constrain a name to a single line by applying .lineLimit(1). Then, to avoid truncation (with ellipses), you can allow the font to shrink by using .minimumScaleFactor.

    VStack {
        if let uiImage = UIImage(named: card.imageName) {
            Image(uiImage: uiImage)
                .resizable()
                .scaledToFill()
                .clipped()
        }
        VStack {
            Text(card.name)
            Text(card.arcana.rawValue)
        }
        .lineLimit(1)
        .hidden() // Hide the footprint
        .overlay {
            VStack {
                Text(card.name)
                Text(card.arcana.rawValue)
            }
            .lineLimit(1)
            .minimumScaleFactor(0.5)
        }
    }
    

    Screenshot

    2. Allow names to wrap

    If you want to keep the font size constant and allow names to wrap, then the footprint needs to be based on the longest possible name:

    VStack {
        if let uiImage = UIImage(named: card.imageName) {
            Image(uiImage: uiImage)
                .resizable()
                .scaledToFill()
                .clipped()
        }
        VStack {
            Text("The longest possible name")
            Text(card.arcana.rawValue)
        }
        .hidden() // Hide the footprint
        .overlay(alignment: .top) {
            VStack {
                Text(card.name)
                Text(card.arcana.rawValue)
            }
        }
    }
    

    ConstantFontSize

    Regarding your question about whether to use LazyVGrid or Grid, this is probably answered by the documentation:

    Only use a lazy grid if profiling your app shows that a Grid view performs poorly because it tries to load too many views at once.

    If you use a Grid then I think you are responsible for ordering the contents into rows, so it is a bit more overhead. But if you want all columns of the grid to have the same width then you don't really need to use a Grid at all. You could use a combination of VStack (for the overall container) and HStack (for the rows) and set .frame(maxWidth: .infinity) on every grid item. This will cause them to share the width of an HStack equally. This way, you could probably achieve the same result as both the solutions above, but without needing any hidden footprints.


    EDIT The explanation for the first suggestion (using .scaledToFit and alignment .top) is as follows:

    The size of this item is the size of the grid with spacing and inflexible items removed, divided by the number of flexible items, clamped to the provided bounds.

    ...so the columns will each be given one third of the available width.