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)?
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.
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: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
.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:
Regarding your question about whether to use
LazyVGrid
orGrid
, this is probably answered by the documentation: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 aGrid
at all. You could use a combination ofVStack
(for the overall container) andHStack
(for the rows) and set.frame(maxWidth: .infinity)
on every grid item. This will cause them to share the width of anHStack
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:GridItem
have a.flexible
size. According to the documentation, this means:...so the columns will each be given one third of the available width.
By using
.scaledToFit
, it seems that the size of the image is determined by the width and all images have the same size.When the label of an image needs to wrap, it causes the height of the row to increase. This means, if there are any cells in the same row which only have 2-line labels then these cells will have some spare vertical space. By using
alignment: .top
, the content of the cell is aligned at the top and the spare space is all at the bottom. This keeps all the images in the row aligned with each other.When
.scaledToFill
is used instead ofscaledToFit
and a label needs to wrap, it seems the images are scaled a bit smaller to give more space for the labels. This spoils the layout. This surprises me a bit, I would have expected the images to be cropped top-and-bottom. But if.scaledToFit
gives the required result and works reliably then it doesn't really matter how it looks with.scaledToFill
.