4

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

squarehippo10
  • 1,855
  • 1
  • 15
  • 45

2 Answers2

4

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
                }
            }
        }
    }
}
pawello2222
  • 46,897
  • 22
  • 145
  • 209
  • I don't fully understand your solution just yet, but it looks very promising. – squarehippo10 Nov 01 '20 at 21:36
  • @squarehippo10 Here is a very good tutorial about view preferences: [Inspecting the View Tree – Part 1: PreferenceKey](https://swiftui-lab.com/communicating-with-the-view-tree-part-1/) – pawello2222 Nov 01 '20 at 21:37
1

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
     })
Asperi
  • 228,894
  • 20
  • 464
  • 690
  • Okay, so with your solution I can get the total height of the main view. And with the code I provided, I can get the height of each subview. The goal is to only include the subviews that will fit in the main view. So how do I stop adding views once the limit has been reached? – squarehippo10 Oct 23 '20 at 20:54
  • What do you mean by *fit in the main view*? In SwiftUI *Stack container view is as big as its content, so all your subviews *fit* into main view. – Asperi Oct 24 '20 at 04:47
  • You are correct. In my case, however, the height is fixed, so only a certain number of subviews will fit. I added an image to help clarify my issue. Thanks! – squarehippo10 Oct 24 '20 at 14:04
  • If you have defined height then you don't need to calculate anything, just apply height to stack and clip internals, like `VStack{}.frame(height: 792).clipped()` - that will give you what you described. – Asperi Oct 27 '20 at 04:31
  • That could work, but then how would I know where I was in the array when the clipping occurred? The point of keeping a running total is so that the remaining subviews could be added to a second main view. Thank you, by the way - I appreciate your persistence. – squarehippo10 Oct 27 '20 at 17:35