GeometryReader to calculate total height of views

1.2k views Asked by At

I have a main view with a fixed width and height because this view will be converted into a PDF. Next, I have an array of subviews that will be vertically stacked in the main view. There may be more subviews available than can fit in the main view so I need a way to make sure that I don't exceed the main view's total height. For example, if I have eight subviews in my array but only three will fit into the main view, then I need to stop adding subviews after three. The remaining subviews will start the process over on a new main view.

My problem is, if I use GeometryReader to get the height of a view, first I have to add it. But once the view is added, it’s too late to find out if it exceeded the total height available.

Below shows how I get the height of each view as it's being added. Which is not much to go on, I know, but I'm pretty stuck.

Update: My current strategy is to create a temporary view where I can add subviews and return an array with only the ones that fit.

struct PDFView: View {
    var body: some View {
       VStack {
         ForEach(tasks) { task in
            TaskRowView(task: task)
              .overlay(
                 GeometryReader { geo in
                    // geo.size.height - to get height of current view
                 })
         }
       }
       .layoutPriority(1)
   }
}

enter image description here

2

There are 2 answers

2
pawello2222 On BEST ANSWER

A possible solution is to use View Preferences.

You can calculate the total height of your items and in onPreferenceChange increase their count by 1 (until the totalHeight reaches maxHeight).


Here is the full implementation:

  1. Create a custom PreferenceKey for retrieving view height:
struct ViewHeightKey: PreferenceKey {
    static var defaultValue: CGFloat = 0

    static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
        value += nextValue()
    }
}
struct ViewGeometry: View {
    var body: some View {
        GeometryReader { geometry in
            Color.clear
                .preference(key: ViewHeightKey.self, value: geometry.size.height)
        }
    }
}
  1. Create a TaskRowView (of any height):
struct TaskRowView: View {
    let index: Int

    var body: some View {
        Text("\(index)")
            .padding()
            .background(Color.red)
    }
}
  1. Use the custom PreferenceKey in your view:
struct ContentView: View {
    @State private var count = 0
    @State private var totalHeight: CGFloat = 0
    @State private var maxHeightReached = false
    let maxHeight: CGFloat = 300

    var body: some View {
        VStack {
            Text("Total height: \(totalHeight)")
            VStack {
                ForEach(0 ..< count, id: \.self) {
                    TaskRowView(index: $0)
                }
            }
            .background(ViewGeometry())
            .onPreferenceChange(ViewHeightKey.self) {
                totalHeight = $0
                print(count, totalHeight)
                guard !maxHeightReached else { return }
                if $0 < maxHeight {
                    count += 1
                } else {
                    count -= 1
                    maxHeightReached = true
                }
            }
        }
    }
}
5
Asperi On

You can get total height in one place as shown below:

   VStack {
     ForEach(tasks) { task in
        TaskRowView(task: task)
     }
   }
  .overlay(      
     GeometryReader { geo in
        // geo.size.height // << move it here and get total height at once
     })