Chris Quenelle
Chris Quenelle

Reputation: 841

How do I make a SwiftUI Scroll view shrink-to-content?

I have a SwiftUI GUI with two lists stacked on top of each other. Each list has a variable number of items. I'd like both lists to shrink to fit their contents.

If I use VStack around ForEach, the simple case works (see example below). There is a spacer near the bottom, so the lists shift to the top of the window like I expect.

Now, sometimes the lists are large and I want a Scroll view and a maximum height, but I still want the list to shrink when it has fewer items. But as soon as I add the Scroll view, the Scroll view starts to take up all the space it can. If I assign it a maximum, then it doesn't shrink to fit it's contents anymore.

In the example below (as written) I get the resizing behavior I want (without the max size). If I uncomment the Scroll view, then it consumes all the space, if I uncomment the frame modifier, it works, but the size is fixed.

struct ContentView: View {
    @State var List1: [String] = [  ]
    @State var List2: [String] = [  ]

    var body: some View {
        VStack {
            Button("1-5") {
                List1=[ "1" ]
                List2=[ "a", "b", "c", "d", "e" ]
            }
            Button("3-3") {
                List1=[ "1", "2", "3" ]
                List2=[ "a", "b", "c" ]
            }
            Button("5-1") {
                List1=[ "1", "2", "3", "4", "5" ]
                List2=[ "a" ]
            }
            //ScrollView {
                VStack {
                    ForEach(List1.indices, id: \.self) { idx in
                        Text(List1[idx])
                    }
                }
            //}
            //.frame(maxHeight: 40)
            Text("middle")
            VStack {
                ForEach(List2.indices, id: \.self) { idx in
                    Text(List2[idx])
                }
            }
            Spacer()
            Text("last")
        }
    }
}

Upvotes: 6

Views: 3868

Answers (2)

Victor Bogdan
Victor Bogdan

Reputation: 2050

Another option is to apply the .fixedSize(horizontal: false, vertical: true) modifier to the ScrollView, to request an ideal vertical size for it.

In the screenshot, i've marked the ScrollView with a red border. I've used the body code in the original post.

Screenshot of code in original question

Upvotes: 0

Adrien
Adrien

Reputation: 1927

You need PreferenceKey to calculate the size of your ScrollView content. Here a getSize function that can help you :

struct SizePreferenceKey: PreferenceKey {
    static var defaultValue: CGSize = .zero

    static func reduce(value: inout CGSize, nextValue: () -> CGSize) {
        value = nextValue()
    }
}

struct SizeModifier: ViewModifier {
    private var sizeView: some View {
        GeometryReader { geometry in
            Color.clear.preference(key: SizePreferenceKey.self, value: geometry.size)
        }
    }

    func body(content: Content) -> some View {
        content.overlay(sizeView)
    }
}

extension View {
    func getSize(perform: @escaping (CGSize) -> ()) -> some View {
        self
            .modifier(SizeModifier())
            .onPreferenceChange(SizePreferenceKey.self) {
                perform($0)
            }
    }
}

You have to compare the height of your content (with getSize) and the height of the ScrollView (with GeometryReader), and set the frame accordingly :

struct SwiftUIView12: View {
    @State private var items: [String] = ["One", "Two", "Three"]
    @State private var scrollViewSize: CGSize = .zero
    var body: some View {
        GeometryReader { proxy in
            ScrollView {
                ForEach(items, id: \.self) { item in
                    Text(item)
                        .padding()
                }
                .frame(maxWidth: .infinity)
                .getSize {scrollViewSize = $0}
            }
            .frame(height: scrollViewSize.height < proxy.size.height ? scrollViewSize.height : .none )
            .background(Color.blue.opacity(0.2))
        }
        .navigationTitle("Test")
        .toolbar {
            Button("Many items") {
                items = (1 ... 30).map { _ in String.random(length: 10) }
            }
        }
    }
}

enter image description here

Upvotes: 6

Related Questions