'Dynamically size a GeometryReader height based on its elements

I'm trying to do something that's pretty straight forward in my mind.

I want a subview of a VStack to dynamically change its height based on its content (ProblematicView in the sample below).

It usually works pretty well, but in this case ProblematicView contains a GeometryReader (to simulate a HStack over several lines).

However, the GeometryReader greedily takes all the space it can (the expected behavior happens if you remove the GeometryReader and it's content). Unfortunately on the Parent view (UmbrellaView in the sample below), the UmbrellaView VStack assigns 50% of itself to the ProblematicView instead of the minimal size to display the content of the view.

I've spend a few hours playing with min/ideal/maxHeight frame arguments, to no avail.

Is what I'm trying to achieve doable?

I added pictures at the bottom to clarify visually.

struct UmbrellaView: View {
    var body: some View {
        VStack(spacing: 0) {
            ProblematicView()
            .background(Color.blue)

            ScrollView(.vertical) {
                Group {
                    Text("A little bit about this").font(.system(size: 20))
                    Divider()
                }
                Group {
                    Text("some").font(.system(size: 20))

                    Divider()
                }
                Group {
                    Text("group").font(.system(size: 20)).padding(.bottom)
                    Divider()
                }
                Group {
                    Text("content").font(.system(size: 20))
                }
            }

        }
    }
}


struct ProblematicView: View {

    var body: some View {
        let tags: [String] = ["content", "content 2 ", "content 3"]
        var width = CGFloat.zero
        var height = CGFloat.zero

        return VStack(alignment: .center) {
            Text("Some reasonnably long text that changes dynamically do can be any size").background(Color.red)
            GeometryReader { g in
                ZStack(alignment: .topLeading) {
                    ForEach(tags, id: \.self) { tag in
                        TagView(content: tag, color: .red, action: {})
                            .padding([.horizontal, .vertical], 4)
                            .alignmentGuide(.leading, computeValue: { d in
                                if (abs(width - d.width) > g.size.width)
                                {
                                    width = 0
                                    height -= d.height
                                }
                                let result = width
                                if tag == tags.last! {
                                    width = 0 //last item
                                } else {
                                    width -= d.width
                                }
                                return result
                            })
                            .alignmentGuide(.top, computeValue: {d in
                                let result = height
                                if tag == tags.last! {
                                    height = 0 // last item
                                }
                                return result
                            })
                    }
                }.background(Color.green)
            }.background(Color.blue)
        }.background(Color.gray)
    }
}

struct TagView: View {
    let content: String
    let color: Color
    let action: () -> Void?

    var body: some View {
        HStack {
            Text(content).padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
            Button(action: {}) {
                Image(systemName: "xmark.circle").foregroundColor(Color.gray)
            }.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 7))
        }
        .background(color)
        .cornerRadius(8.0)
    }
}

struct ProblematicView_Previews: PreviewProvider {
    static var previews: some View {
        return ProblematicView()
    }
}


struct UmbrellaView_Previews: PreviewProvider {
    static var previews: some View {
        return UmbrellaView()
    }
}

ProblematicView Preview

UmbrellaView Preview



Solution 1:[1]

Due to "hen-egg" problem in nature of GeometryReader the solution for topic question is possible only in run-time, because 1) initial height is unknown 2) it needs to calculate internal size based on all available external size 3) it needs to tight external size to calculated internal size.

So here is possible approach (with some additional fixes in your code)

previewrun-timerun-time2

  1. Preview 2-3) Run-time

Code:

struct ProblematicView: View {

    @State private var totalHeight = CGFloat(100) // no matter - just for static Preview !!
    @State private var tags: [String] = ["content", "content 2 ", "content 3", "content 4", "content 5"]

    var body: some View {
        var width = CGFloat.zero
        var height = CGFloat.zero

        return VStack {
            Text("Some reasonnably long text that changes dynamically do can be any size").background(Color.red)
            VStack { // << external container
                GeometryReader { g in
                    ZStack(alignment: .topLeading) { // internal container
                        ForEach(self.tags, id: \.self) { tag in
                            TagView(content: tag, color: .red, action: {
                                    // self.tags.removeLast()         // << just for testing
                                })
                                .padding([.horizontal, .vertical], 4)
                                .alignmentGuide(.leading, computeValue: { d in
                                    if (abs(width - d.width) > g.size.width)
                                    {
                                        width = 0
                                        height -= d.height
                                    }
                                    let result = width
                                    if tag == self.tags.last! {
                                        width = 0 //last item
                                    } else {
                                        width -= d.width
                                    }
                                    return result
                                })
                                .alignmentGuide(.top, computeValue: {d in
                                    let result = height
                                    if tag == self.tags.last! {
                                        height = 0 // last item
                                    }
                                    return result
                                })
                        }
                    }.background(Color.green)
                    .background(GeometryReader {gp -> Color in
                        DispatchQueue.main.async {
                            // update on next cycle with calculated height of ZStack !!!
                            self.totalHeight = gp.size.height
                        }
                        return Color.clear
                    })
                }.background(Color.blue)
            }.frame(height: totalHeight)
        }.background(Color.gray)
    }
}

struct TagView: View {
    let content: String
    let color: Color
    let action: (() -> Void)?

    var body: some View {
        HStack {
            Text(content).padding(EdgeInsets(top: 5, leading: 5, bottom: 5, trailing: 5))
            Button(action: action ?? {}) {
                Image(systemName: "xmark.circle").foregroundColor(Color.gray)
            }.padding(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 7))
        }
        .background(color)
        .cornerRadius(8.0)
    }
}

backup

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1