0
votes

I'm trying to make a view which holds an image loaded asynchronously from a network request. Before the image loads, I want to show a placeholder view which has a fixed size. When the image loads, I want to replace this placeholder view with the image, scaled to fit inside the frame of the placeholder view, but then I want the parent view to shrink to match the size of this image. I can't figure out how to do this last part.

Right now, it looks like this:

struct ItemCell: View {
    var body: some View {
        Group {
            CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
                .aspectRatio(contentMode: .fit)
                .frame(minWidth: 0, maxWidth: 150, minHeight: 0, maxHeight: 190, alignment: .bottomLeading)
        }.background(Color.red) // To show that the view isn't resizing properly

    }
}

struct PlaceholderView: View {
    var body: some View {
        ZStack {
            RoundedRectangle(cornerRadius: 3, style: .continuous)
                .frame(width: 150, height: 190)
                .foregroundColor(Color(.secondarySystemBackground))
            Image(systemName: "globe")
                .resizable()
                .scaledToFit()
                .frame(width: 50)
                .foregroundColor(.secondary)
        }
    }
}

The CustomImageView is adapted from this article on loading images asynchronously. The ItemCells are placed in a horizontal ScrollView. When I test this, it:

  • correctly displays the placeholder view before the image is loaded;
  • resizes the image so it maintains its aspect ratio and fits inside the 150x190 frame, but has a weird animation where some of the images shrink and then expand back; also, some of the images seem to shrink too much;
  • does not resize the parent view to match the size of the image properly, but instead retains the full original height and some (?) extra width on some cells.

These two problems are shown in the gif below, with blue images and a red background. Notice the extra height on the first and third cells, and the extra width on the second. Also, note that the first image ends up smaller than when it first loads, even though it fit inside the original 150x190 frame at first.

image resizing issue

How can I fix these problems?

1
You have a content mode of .fit and a fixed cell size, so you unless all of your images are 150x190 you are going to end up with some space around the image. Try putting your CustomImageView in HStack & VStack with some Spacer()s so that there is the ability for the image view to shrink - Paulw11
I know that if the aspect ratio of the image doesn't match, it won't fully fill a 150x190 frame, but shouldn't the CustomImageView shrink to match the size of its child view? It seems to do this already, but not fully (like how the second cell in the image doesn't stay the full width of 190, but there's still a small extra amount of space on the right. And it never does this w.r.t. the height. - ceyx
No because it’s vertical dimension is being constrained by the Group. That is why you need a spacer to soak up the extra height. - Paulw11
@Paulw11 I switched out the Group for VStack { Spacer() HStack { CustomImageCell() Spacer() } } ... to no avail. This just makes the images slightly smaller so there's always extra space on the top and right. Why does the Group/VStack/etc have a fixed height? I thought that with SwiftUI, in the absence of a programmatically defined frame, the parent view adopts the size of the child view? - ceyx

1 Answers

1
votes

Figured out how to do it. There were several problems with my original code. First, the ItemCells used in the ScrollView should be modified with the .fixedSize() view modifier, like so:

ScrollView(...) {
    HStack(...) {
        ForEach(...) { ...
            ItemCell()
                .fixedSize()
        }
    }
}

Then, changing the frame of the CustomImageCell to be use idealHeight instead of maxHeight and making the Group a VStack with a Spacer() to push everything to the bottom, as @Paulw11 had suggested in comments:

struct ItemCell: View {
    var body: some View {
        VStack {
            Spacer()
            CustomImageView(from: imageURL, placeholder: PlaceholderView(), config: { $0.resizable() })
                .aspectRatio(contentMode: .fit)
                .frame(maxWidth: 150, idealHeight: 190)
        }

    }
}

These changes fix both the image resizing animation issue and the extra space issue.