Reputation: 21883
I'm messing about with a grid where I want the first column to shrink to the width of the content and the second column to fill the remaining space. Using IB and layout constraints this sort of thing is relatively simple, but it's got me stumped in SwiftUI.
Basically I have code like this:
@State var sliderValue1: Float = 0.5
@State var sliderValue2: Float = 0.25
var body: some View {
let col1 = GridItem(alignment: .leading)
let col2 = GridItem()
LazyVGrid(columns: [col1, col2]) {
Text("Short").border(Color.green)
Slider(value: $sliderValue1, in: 0.0 ... 1.0, step: 0.25).border(Color.red)
Text("Rather long").border(Color.green)
Slider(value: $sliderValue2, in: 0.0 ... 1.0, step: 0.25).border(Color.red)
}
.border(Color.blue)
}
As you can see the two columns are the same size which is not what I want. What I'm trying to do is shrink the first column's width to that of the "Rather long" label. I can't specify a size using .fixed(...)
and I've tried all sorts of .flexible(...)
definitions with no luck.
Anyone got a column to resize to fit the content?
Upvotes: 9
Views: 3093
Reputation: 20975
Grid(alignment: .leading, horizontalSpacing: 10, verticalSpacing: 10) {
GridRow() {
Text("Short")
.background {
Color.yellow
}
Slider(value: $sliderValue1, in: 0.0 ... 1.0, step: 0.25).border(Color.red)
.frame(maxWidth: .infinity)
.background {
Color.blue
}
}
GridRow() {
Text("Rather long")
.background {
Color.yellow
}
Slider(value: $sliderValue2, in: 0.0 ... 1.0, step: 0.25).border(Color.red)
.frame(maxWidth: .infinity)
.background {
Color.blue
}
}
}
.frame(maxWidth: .infinity)
.background {
Color.orange
}
Upvotes: 3
Reputation: 21883
Thanks to @asperi I dug around some more in the mentioned posts and came up with the following code. Basically I ended up throwing out the grid because I found that even with preference keys passing values back up the hierarchy, the grid was setting the width and not allow the labels to calculate the sizes I needed. So vertical and horizontal stacks were the answer, allowing things to size correctly.
// The preference key used to advise parent views of a change in value.
struct LabelWidthPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = .zero
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
print("Reducing \(value) \(nextValue())")
value = max(value, nextValue())
}
}
// A modifier that wraps a view in a geometry reader.
// This allows us to measure the size of a view and send that back up the view heirarchy
// so the display can be sized to fit.
struct LabelWidthObserver: ViewModifier {
func body(content: Content) -> some View {
return content.background {
GeometryReader { geometry in
background(geometry: geometry)
}
}
}
private func background(geometry: GeometryProxy) -> some View {
Color.clear.preference(key: LabelWidthPreferenceKey.self, value: geometry.size.width)
}
}
struct ContentView: View {
@State private var sliderValue1: Float = 0.5
@State private var sliderValue2: Float = 0.25
@State private var labelWidth: CGFloat = 20.0
var body: some View {
VStack {
aspect(withTitle: "Short")
aspect(withTitle: "Rather long")
}
.border(Color.blue).padding(5)
.onPreferenceChange(LabelWidthPreferenceKey.self) {
print("Incoming label width \($0)")
labelWidth = max(labelWidth, $0)
}
}
@ViewBuilder
func aspect(withTitle title: String) -> some View {
HStack {
Text(title)
.border(Color.green)
.frame(minWidth: labelWidth, alignment: .leading)
.modifier(LabelWidthObserver())
Slider(value: $sliderValue1, in: 0.0 ... 1.0, step: 0.25).border(Color.red)
}
.border(.yellow).padding(5)
}
}
Which produces:
In which you can see that the labels and sliders are now acting like they are in columns even though they are not.
Upvotes: 5