2

I'm trying to create a scrollable grid of items. I create a custom view called GridView which uses GeometryReader to divide the space into columns and rows in HStacks and VStacks. But for some reason, its size shrinks to almost nothing when inside a ScrollView. In the screenshot you see the GridView (reddish) and it's parent VStack (greenish) have shrunk. The items inside the grid are still visible, rendered outside its area, but things do not scroll properly.

Why is the GridView not the size required to contain its items? If it did, I think this UI would scroll properly.

enter image description here

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack {
                Text("Section 1")
                GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
                    Text("Item \(num)")
                }
                .background(Color.red.opacity(0.2))
            }.background(Color.green.opacity(0.2))
        }.background(Color.blue.opacity(0.2))
    }
}

struct GridView<Content>: View where Content: View {
    var columns: Int
    let items: [Int]
    let content: (Int) -> Content

    init(columns: Int, items: [Int], @ViewBuilder content: @escaping (Int) -> Content) {
        self.columns = columns
        self.items = items
        self.content = content
    }
    var rowCount: Int {
        let (q, r) = items.count.quotientAndRemainder(dividingBy: columns)
        return q + (r == 0 ? 0 : 1)
    }
    func elementFor(_ r: Int, _ c: Int) -> Int? {
        let i = r * columns + c
        if i >= items.count { return nil }
        return items[i]
    }
    var body: some View {
        GeometryReader { geo in
            VStack {
                ForEach(0..<self.rowCount) { ri in
                   HStack {
                        ForEach(0..<self.columns) { ci in
                            Group {  
                                if self.elementFor(ri, ci) != nil {
                                    self.content(self.elementFor(ri, ci)!)
                                        .frame(width: geo.size.width / CGFloat(self.columns),
                                               height: geo.size.width / CGFloat(self.columns))
                                } else {
                                    Text("")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
4
  • I tested your code with latest xcode and iOS and it scrolls for me. What do you mean by not scrollable? Commented Feb 3, 2020 at 22:02
  • I mean I can't see items 13 and 15, because the ScrollView thinks the GridView is only the size of the red area (10pt high), so it is not scrolling, because it thinks it doesn't need to. How big is the red area on your screen (the GridView)? It should encompass the items. Commented Feb 3, 2020 at 22:14
  • oh I see, I looked at it on a bigger screen iPhone and it seemed as if it scrolled. But it does not, it just scrolls that bare minimum when you have no elements in your scrollview Commented Feb 4, 2020 at 7:48
  • I have a same problem with using GeometryReader. I have tried using fixedSize according to this article but no luck for me. Commented Feb 4, 2020 at 10:29

1 Answer 1

1

GeometryReader is a container view that defines its content as a function of its own size and coordinate space. Returns a flexible preferred size to its parent layout.

in

struct ContentView: View {
    var body: some View {
        ScrollView {
            VStack {
                Text("Section 1")
                GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
                    Text("Item \(num)")
                }
                .background(Color.red.opacity(0.5))
            }.background(Color.green.opacity(0.2))
        }.background(Color.blue.opacity(0.2))
    }
}

What is the height of GridView? Hm ... number of lines multiplied by height of grid cell, which is height of GridView divided by number of lines ...

The equation has no solution :-)

Yes I can see, you defined height of grid cell as width of GridView divided by number of columns, but SwiftUI is not so clever.

You have to calculate the height of the GridView. I fixed number of rows to 4 to simplify it ...

struct ContentView: View {
    var body: some View {
        GeometryReader { g in
        ScrollView {
            VStack {
                Text("Section 1")
                GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
                    Text("Item \(num)")
                }.frame(height: g.size.width / 2 * CGFloat(4))
                .background(Color.red.opacity(0.5))
            }.background(Color.green.opacity(0.2))
        }.background(Color.blue.opacity(0.2))
        }
    }
}

You better remove the geometry reader and grid cell frame from GridView.body and set the size of cell in your content view.

struct ContentView: View {
    var body: some View {
        GeometryReader { g in
        ScrollView {
            VStack {
                Text("Section 1")
                GridView(columns: 2, items: [1, 3, 5, 7, 9, 11, 13, 15]) { num in
                    Text("Item \(num)").frame(width: g.size.width / 2, height: g.size .width / 2)
                }
                .background(Color.red.opacity(0.5))
            }.background(Color.green.opacity(0.2))
        }.background(Color.blue.opacity(0.2))
        }
    }
}

As you can see, there is no need to define height of GridView, the equation has solution now.

Sign up to request clarification or add additional context in comments.

8 Comments

I don't understand what you're saying about the "equation" not having a solution. It has a simple solution. On my device the GeometryReader passes in a width of 320, so if you put 20 items (for example) in the array, then the height of the content in the GridView is 320/nCols * nRows = 320/2*10 = 1600. You can prove this isn't the cause by skipping the entire calculation. Instead, just hardcode the frame(height: 1600) directly on the VStack inside the GeometryReader. It still won't layout right. [continued next comment]
... So the problem is that height of the VStack inside the GeometryReader is not being used for the height of the GridView. No one (including Apple DTS) has been able to explain why, so I think this is a bug in SwiftUI. There is a workaround using Preferences to pass the height back up the view hierarchy, but it's so ugly I almost prefer to do what you did and set the height from outside the GridView. As a long term solution though, that is horrible. SwiftUI needs to allow custom Views to calculate their own height, even if they are in a ScrollView.
@RobN very similar problem and with explanation stackoverflow.com/questions/60474057/…
@RobN the idea is that the child View has to decide its size, GeometryReader gives you the maximal size available. What is max size of ScrollView? That is why I wrote that the equation doesn't have a solution. VStack in ScrollView .. What is the size of VStack? etc ...
@RobN see, that in my answer the GeometryReader gives you size of space available in parent of ScrollView. This is "fixed" value, from which you could calculate something.
|

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.