Dave Abrahams explained some of the mechanics of SwiftUI layouts in his WWDC19 talk about Custom Views, but he left out some bits and I have trouble getting my views properly sized.
Is there a way for a View to tell its container that it is not making any space demands, but it will use all the space it is given? Another way to say it is that the container should hug its subviews.
Concrete example, I want something like c:
If you have some Text
s inside a VStack
like in a), the VStack
will adopt it's width to the widest subview.
If you add a Rectangle
though as in b), it will expand as much as it can, until the VStack
fills its container.
This indicates that Text
s and Rectangle
s are in different categories when it comes to layout, Text
has a fixed size and a Rectangle
is greedy. But how can I communicate this to my container if I'm making my own View
?
The result I actually want to achieve is c). VStack should ignore Rectangle (or my custom view) when it determines its size, and then once it has done that, then it should tell Rectangle, or my custom view, how much space it can have.
Given that SwiftUI seems to layout bottom-up, maybe this is impossible, but it seems that there should be some
way to achieve this.
So I actually found a way to do this. First I tried putting Spacers around the views in various configurations, to try to push it together, but that didn't work. Then I realised I could perhaps use the .background modifier, and that actually did work. It seems to let the owning view calculate its size first, and then just takes that as its frame, which is exactly what I want.
This is just an example with some hacks to get the right height, but that is a small detail, and in my particular use case it is not needed. Probably not here either if you're clever enough.
var body: some View {
VStack(spacing: 10) {
Text("Short").background(Color.green)
Text("A longer text").background(Color.green)
Text("Dummy").opacity(0)
}
.background(backgroundView)
.background(Color.red)
.padding()
.background(Color.blue)
}
var backgroundView: some View {
VStack(spacing: 10) {
Spacer()
Spacer()
Rectangle().fill(Color.yellow)
}
}
The blue view and all the color backgrounds are of course just to make it easier to see. This code produces this:
There is no modifier (AFAIK) to accomplish this, so here's my approach. If this is something you are going to use too often, it could be worth creating your own modifier.
Also note that here I am using standard preferences, but anchor preferences are even better. It is a heavy topic to explain here. I've written an article that you can check here: https://swiftui-lab.com/communicating-with-the-view-tree-part-1/
You can use the code below to accomplish what you are looking for.
import SwiftUI
struct MyRectPreference: PreferenceKey {
typealias Value = [CGRect]
static var defaultValue: [CGRect] = []
static func reduce(value: inout [CGRect], nextValue: () -> [CGRect]) {
value.append(contentsOf: nextValue())
}
}
struct ContentView : View {
@State private var widestText: CGFloat = 0
var body: some View {
VStack {
Text("Hello").background(RectGetter())
Text("Wonderful World!").background(RectGetter())
Rectangle().fill(Color.blue).frame(width: widestText, height: 30)
}.onPreferenceChange(MyRectPreference.self, perform: { prefs in
for p in prefs {
self.widestText = max(self.widestText, p.size.width)
}
})
}
}
struct RectGetter: View {
var body: some View {
GeometryReader { geometry in
Rectangle()
.fill(Color.clear)
.preference(key: MyRectPreference.self, value: [geometry.frame(in: .global)])
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With