1

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("")
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
Rob N
  • 11,371
  • 11
  • 72
  • 126
  • I tested your code with latest xcode and iOS and it scrolls for me. What do you mean by not scrollable? – iSpain17 Feb 03 '20 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. – Rob N Feb 03 '20 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 – iSpain17 Feb 04 '20 at 07:48
  • I have a same problem with using GeometryReader. I have tried using `fixedSize` according to [this article](https://sarunw.com/tips/intrinsic-content-size-in-swiftui/) but no luck for me. – gujci Feb 04 '20 at 10:29

1 Answers1

0

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.

user3441734
  • 13,374
  • 2
  • 25
  • 44
  • 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] – Rob N Mar 04 '20 at 15:40
  • 1
    ... 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. – Rob N Mar 04 '20 at 15:40
  • @RobN very similar problem and with explanation https://stackoverflow.com/questions/60474057/swiftui-reduce-spacing-of-rows-in-a-list-to-null/60477215#60477215 – user3441734 Mar 04 '20 at 16:09
  • @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 ... – user3441734 Mar 04 '20 at 16:15
  • @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. – user3441734 Mar 04 '20 at 16:18
  • @RobN in general, the size of ScrollView depends only on its children, up to infinity, doesn't' matter, if it is scrollable horizontally or vertically. It shrinks around all children. – user3441734 Mar 04 '20 at 16:23
  • Okay, I see what you mean about the ScrollView making it so there is no maximum height to pass in via GeometryReader. I think this is not handled correctly by SwiftUI. It should pass in CGFloat.infinity, or some value to indicate the flexibility of the ScrollView (instead it's passing in a seemingly arbitrary height of 10, in the GeometryProxy). And it should obey whatever the contents of the GeometryReader say their height is. Why not, right? Anyway, your workaround of setting the height outside the GeometryReader makes sense. – Rob N Mar 04 '20 at 16:33
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/209011/discussion-between-user3441734-and-rob-n). – user3441734 Mar 04 '20 at 16:34