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")
}
}
}
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) }
}
}
}
}