swiftswiftuiswiftui-listswiftui-zstack

Prevent last rows in List to be covered by other view in ZStack


I have a List partially covered by a translucent view (let's call it the overlay). My problem is that for long lists last rows are not accessible since they are covered by the overlay.

I'm using a ZStack to layout the final view. I've thought about adding some kind of padding on last rows that could make the list content a bit larger, so it can scroll completely out of the overlay, but I don't know how, or even if using ZStack is the correct way to doing for lists.

what I get

import SwiftUI

struct ListWithBottomOverlay: View {
  var body: some View {
    GeometryReader { proxy in
      ZStack {
        List {
          ForEach(1..<20) { number in
            Text("\(number)")
          }
        }

        VStack {
          Spacer().frame(maxHeight: .infinity)

          VStack {
            HStack {
              Button(action: {}, label: { Text("Hello") })
                .frame(minHeight: 100)
            }
            HStack {
              Button(action: {}, label: { Text("World!") })
                .frame(minHeight: 100)
            }
          }
          .frame(maxWidth: .infinity)
          .background(Color(.yellow).opacity(0.8))
        }
      }
    }
  }
}

struct ListWithBottomOverlay_Previews: PreviewProvider {
  static var previews: some View {
    ListWithBottomOverlay()
  }
}

I apologize if it is a duplicate question, I've just started to learn SwiftUI so I'm a bit lost yet about how to search for correct terms.


Solution

  • The possible solution is to calculate height of overlay area and add some transparent view with that height to the bottom of the list.

    Here is a demo of approach using view preferences. Tested with Xcode 12 / iOS 14

    demo

    struct ListWithBottomOverlay: View {
      @State private var height = CGFloat.zero
      var body: some View {
        GeometryReader { proxy in
          ZStack {
            List {
              ForEach(1..<20) { number in
                Text("\(number)")
              }
              Color.clear.frame(height: height)  // injected empty space
            }
    
            VStack {
              Spacer().frame(maxHeight: .infinity)
    
              VStack {
                HStack {
                  Button(action: {}, label: { Text("Hello") })
                    .frame(minHeight: 100)
                }
                HStack {
                  Button(action: {}, label: { Text("World!") })
                    .frame(minHeight: 100)
                }
              }
              .frame(maxWidth: .infinity)
              .background(GeometryReader {
                    // use color filled area in background to read covered frame
                    Color(.yellow).opacity(0.8)
                        .edgesIgnoringSafeArea(.bottom)
                        .preference(key: ViewHeightKey.self, value: $0.frame(in: .local).size.height)
                })
            }
          }
          .onPreferenceChange(ViewHeightKey.self) {
            // view preferences transferred in one rendering cycle and
            // give possibility to update state
            self.height = $0
          }
        }
      }
    }
    
    struct ViewHeightKey: PreferenceKey {
        typealias Value = CGFloat
        static var defaultValue = CGFloat.zero
        static func reduce(value: inout Value, nextValue: () -> Value) {
            value += nextValue()
        }
    }