1

I have a simple grid view where I have cells that show a square image with a couple of lines of text below it.

I'm trying to get the image to fill the square, clipped and not squashed in any way. I have got close, but the image seems to be squashed:

enter image description here

struct CollectionsView: View {
    @StateObject var viewModel: CollectionsViewModel = CollectionsViewModel()
    var body: some View {
        ScrollView {
            let columns = [GridItem(spacing: 16), GridItem(spacing: 16)]
            LazyVGrid(columns: columns) {
                ForEach(viewModel.collections) { collection in
                    Button {
                        print("go to collection")
                    } label: {
                        CollectionsViewCell(collection: collection)
                    }
                    .onAppear {
                        if collection == viewModel.collections.last {
                            Task {
                                try await viewModel.getCollections(nextPage: true)
                            }
                        }
                    }
                }
            }
            .padding(16)
        }
        .navigationTitle("Collections")
        .toolbar {
            ToolbarItem(placement: .navigationBarTrailing) {
                Button {
                    // more button pressed
                } label: {
                    Image(systemName: "ellipsis.circle")
                }
            }
        }
    }
}

struct CollectionsViewCell: View {
    let collection: HashableCollection
    var body: some View {
        VStack(alignment: .leading) {
            AsyncImage(url: URL(string: collection.imageUrlString)) { image in
                image
                    .resizable()
                    .aspectRatio(1, contentMode: .fill)
            } placeholder: {
                Rectangle()
            }
            .background(.secondary)
            .clipped()
            .cornerRadius(11)
            Text(collection.title)
                .lineLimit(1)
                .truncationMode(.tail)
            Text("\(collection.itemCount) item\(collection.itemCount == 1 ? "" : "s")")
                .foregroundColor(.secondary)
        }
    }
}

I have tried using Geometry Reader, but it doesn't behave well inside the ScrollView - is there a better way to achieve this?

// UPDATE //

I have come up with solution, but it seems a bit messy... is there a better way?

struct CollectionsView: View {
    @StateObject var viewModel: CollectionsViewModel = CollectionsViewModel()
    var body: some View {
        GeometryReader { geometry in
            ScrollView {
                let spacing: Double = 16
                let columns = [GridItem(spacing: spacing), GridItem(spacing: spacing)]
                LazyVGrid(columns: columns) {
                    ForEach(viewModel.collections) { collection in
                        Button {
                            print("go to collection")
                        } label: {
                            let geometryInfo = GeometryInfo(size: geometry.size, columnCount: Double(columns.count), spacing: spacing)
                            CollectionsViewCell(geometryInfo: geometryInfo, collection: collection)
                        }
                        .onAppear {
                            if collection == viewModel.collections.last {
                                Task {
                                    try await viewModel.getCollections(nextPage: true)
                                }
                            }
                        }
                    }
                }
                .padding(spacing)
            }
            .navigationTitle("Collections")
            .toolbar {
                ToolbarItem(placement: .navigationBarTrailing) {
                    Button {
                        // more button pressed
                    } label: {
                        Image(systemName: "ellipsis.circle")
                    }
                }
            }
        }
    }
}

struct CollectionsViewCell: View {
    let geometryInfo: GeometryInfo
    let collection: HashableCollection
    var body: some View {
        VStack(alignment: .leading) {
            AsyncImage(url: URL(string: collection.imageUrlString)) { image in
                image
                    .resizable()
                    .scaledToFill()
            } placeholder: {
                Rectangle()
            }
            .frame(width: getImageSize(), height: getImageSize())
            .background(.secondary)
            .clipped()
            .cornerRadius(11)
            Text(collection.title)
                .lineLimit(1)
                .truncationMode(.tail)
            Text("\(collection.itemCount) item\(collection.itemCount == 1 ? "" : "s")")
                .foregroundColor(.secondary)
        }
    }
    func getImageSize() -> Double {
        (geometryInfo.size.width - ((geometryInfo.columnCount + 1) * geometryInfo.spacing)) / geometryInfo.columnCount
    }
}

2 Answers 2

1

The trick is to make the image accept a given size coming from higher in the view hierarchy. This solution puts the image in a ZStack together with something that does accept given sizes, e.g. just a Color.

Then make the inner image .fill – and give it a lower .layoutPriority, so it accepts the size defines by the color.

Then add .aspectRatio(1, contentMode: .fit) and other modifiers to the outer view.

enter image description here

struct CollectionsViewCell: View {
    let collection: HashableCollection
    var body: some View {
        VStack(alignment: .leading) {
            ZStack { // new ZStack
                AsyncImage(url: URL(string: collection.imageUrlString)) { image in
                    image
                        .resizable()
                        .scaledToFill()
                } placeholder: {
                    Rectangle()
                }
                .layoutPriority(-1) // THIS is the important line

                Color.clear // dummy view to activate layoutPriority on other view
            }
            .clipped()
            .aspectRatio(1, contentMode: .fit)
            .cornerRadius(11)
            
            Text(collection.title)
                .lineLimit(1)
                .truncationMode(.tail)
            Text("\(collection.itemCount) item\(collection.itemCount == 1 ? "" : "s")")
                .foregroundColor(.secondary)
        }
    }
}
Sign up to request clarification or add additional context in comments.

Comments

1

Just use these modifiers for your image:

image
    .resizable()
    .scaledToFill()
    .frame(minHeight: 0, maxHeight: .infinity)
    .aspectRatio(1, contentMode: .fill)

result:

enter image description here

The complete example snippet (starting from your code) is:

struct CollectionsView: View {
    var body: some View {
        ScrollView {
            let spacing: Double = 16
            let columns = [GridItem(spacing: spacing), GridItem(spacing: spacing)]
            LazyVGrid(columns: columns) {
                ForEach(0...10, id: \.self) { collection in
                    CollectionsViewCell()
                }
            }
            .padding(spacing)
        }
    }
}

struct CollectionsViewCell: View {
    var body: some View {
        VStack(alignment: .leading) {
            AsyncImage(url: URL(string: "https://picsum.photos/id/237/200/300")) { image in
                image
                    .resizable()
                    .scaledToFill()
                    .frame(minHeight: 0, maxHeight: .infinity)
                    .aspectRatio(1, contentMode: .fill)
            } placeholder: {
                Rectangle()
            }
            .background(.secondary)
            .clipped()
            .cornerRadius(11)
            Text("Title")
                .lineLimit(1)
                .truncationMode(.tail)
            Text("18 items")
                .foregroundColor(.secondary)
        }
    }
}

1 Comment

this is all well, but you have defined a fixed frame size - I want to have 2 columns regardless of the phone screen size with 16pt spacing

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.