Reputation: 1260
How can I make a parent view's width the width of the smallest child?
I can learn the width of the child view using the answer to this question like so:
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = CGFloat(0)
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = nextValue()
}
typealias Value = CGFloat
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle().fill(Color.clear).preference(key: WidthPreferenceKey.self, value: geometry.size.width)
}
}
}
struct Content: View {
@State var width1: CGFloat = 0
@State var width2: CGFloat = 0
var body: some View {
VStack {
Text("Short")
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.width1 = $0 })
Text("Loooooooooooong")
.background(TextGeometry())
.onPreferenceChange(WidthPreferenceKey.self, perform: { self.width2 = $0 })
}
.frame(width: min(self.width1, self.width2)) // This is always 0
}
}
But the VStack
has always width 0. It's like as if
the line .frame(width: min(self.width1, self.width2))
is being called
before the widths are set.
Any ideas?
Upvotes: 0
Views: 847
Reputation: 9473
This is a logic bug, in that the system is technically doing exactly what you've told it to, but that result is not what you, as the programmer, intended.
Basically, VStack wants to shrink as much as possible to fit the size of its content. Likewise, Text views are willing to shrink as much as possible (truncating their contents) to fit inside their parent view. The only hard requirement you've given is that the initial frame of the VStack have width of 0. So the following sequence happens:
width1
and width2
are initialized to 0VStack
sets its width to min(0, 0)
Text
views inside shrink to width 0WidthPreferenceKey.self
is set to 0.onPreferenceChange
sets width1
and width2
, which are already 0All the constraints are satisfied, and SwiftUI happily stops layout.
Let's modify your code with the following:
WidthPreferenceKey.Value
a typealias to [CGFloat]
instead of CGFloat
. This way, you can set as many preference key setters as you want, and they will just keep accumulating into an array..onPreferenceChange
call, which will find the minimum of all the read values, and set the single @State var width: CGFloat
property.fixedSize()
to the Text
views.Like so:
struct WidthPreferenceKey: PreferenceKey {
static var defaultValue = [CGFloat]()
static func reduce(value: inout [CGFloat], nextValue: () -> [CGFloat]) {
value.append(contentsOf: nextValue())
}
typealias Value = [CGFloat]
}
struct TextGeometry: View {
var body: some View {
GeometryReader { geometry in
return Rectangle()
.fill(Color.clear)
.preference(key: WidthPreferenceKey.self,
value: [geometry.size.width])
}
}
}
struct Content: View {
@State var width: CGFloat = 0
var body: some View {
VStack {
Text("Short")
.fixedSize()
.background(TextGeometry())
Text("Loooooooooooong")
.fixedSize()
.background(TextGeometry())
}
.frame(width: self.width)
.clipped()
.background(Color.red.opacity(0.5))
.onPreferenceChange(WidthPreferenceKey.self) { preferences in
print(preferences)
self.width = preferences.min() ?? 0
}
}
}
Now the following happens:
width
is initialized to 0VStack
sets its width to 0Text
views expand outside the VStack, since we've given permission with .fixedSize()
WidthPreferenceKey.self
is set to [42.5, 144.5]
(or something close).onPreferenceChange
sets width
to 42.5
VStack
sets its width to 42.5
to satisfy its .frame()
modifier.All constraints are satisfied, and SwiftUI stops layout. Note that .clipped()
is keeping the edges of the long Text
view from displaying, even though they are technically outside the bounds of the VStack
.
Upvotes: 2