Here's what I need to do: I have a parent view with a fixed width and 4 children. 2 of the children (1st and 3rd) have fixed width. The others (2nd and 4th) have flexible width. The default interpretation for flexible is that they would share equally the space available, but I don't want this, instead, I want to tell each flexible View how much of the remaining space it should take, for instance, the first should take 2/3 and the second 1/3.
For people familiar with the Flex Layout, this is equivalent to say I want my 2nd View to have flex: 2 and my 4th view to have flex: 1. See the example below:
In the example above, the parent view has the fixed width of 600px. The first child has 100px width, the third has 140px. The remaining space is 360px, so the second child gets 240px (2/3) and the fourth child gets 120px(1/3). If I change the width of the parent view, the size of the second and fourth children will adapt accordingly.
It is extremely simple to do this for the Web, see the code below:
<div style="width: 600; display: flex; flex-direction: row;">
<div style="width: 100; height: 100; background-color: red;"></div>
<div style="flex: 2; height: 100; background-color: green;"></div>
<div style="width: 140; height: 100; background-color: blue;"></div>
<div style="flex: 1; height: 100; background-color: purple;"></div>
</div>
It is also super-simple to do it in Flutter:
Row(
children: [
Container(height: 50, width: 50, color: Colors.red),
Flexible(flex: 2, child: Container(height: 50, color: Colors.green)),
Container(height: 50, width: 80, color: Colors.blue),
Flexible(flex: 1, child: Container(height: 50, color: Colors.purple)),
]
)
It's very similar in Jetpack Compose (Android):
Row {
Row(modifier = Modifier.height(50.dp).width(50.dp).background(color = Color.Red)) {}
Row(modifier = Modifier.height(50.dp).weight(2F).background(color = Color.Green)) {}
Row(modifier = Modifier.height(50.dp).width(80.dp).background(color = Color.Blue)) {}
Row(modifier = Modifier.height(50.dp).weight(1F).background(color = Color.Black)) {}
}
I tried a lot of things, but I couldn't find any simple way of achieving this behavior in SwiftUI. From my understanding, to size the children, SwiftUI first calculates the size of every child that can have its size calculated, i.e. every child that has a fixed size, after that it calculates the remaining space and divide it by the number of children with an undefined size telling each of them this is the maximum size they can occupy (this is the size we read when using a GeometryReader). Is there a way to tell SwiftUI to not divide the space available equally among the children with undefined size, but instead divide it according to a factor specified by each one of them?
Is there a different approach to this in a way I can obtain the same result as the other platforms I mentioned here?
For now, my strategy is much more complex than I'd like it to be. I render the first child with width = 50, the second child with width = 0 (should be flexible), the third child with width = 80, the fourth child with width = 0 (should be flexible) and an additional 5th child with width = .infinity. After the first render, I measure the size of the 5th child and update a state with this value, which represents the remaining space after the children with fixed size have been placed. After the state is updated, I can calculate the sizes of the flexible children and render everything a second time, now with the correct sizes. See the code below:
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGSize = .zero
static func reduce(value: inout CGSize, nextValue: () -> CGSize) {}
}
struct ContentView: View {
private let totalFlexFactor = 3
@State private var freeSpace: CGSize? = nil
func getFlexibleWidth(parentWidth: CGFloat, factor: Int) -> CGFloat {
return (CGFloat(factor) / CGFloat(totalFlexFactor)) * (freeSpace?.width ?? 0)
}
var body: some View {
GeometryReader { parentGeometry in
HStack(spacing: 0) {
HStack() {}
.frame(maxWidth: 50, maxHeight: .infinity)
.background(Color.red)
HStack() {}
.frame(maxWidth: getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 2), maxHeight: .infinity)
.background(Color.green)
HStack() {}
.frame(maxWidth: 80, maxHeight: .infinity)
.background(Color.blue)
HStack() {}
.frame(maxWidth: getFlexibleWidth(parentWidth: parentGeometry.size.width, factor: 1), maxHeight: .infinity)
.background(Color.purple)
if (freeSpace == nil) {
HStack() {}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(
GeometryReader { geometryProxy in
Color.clear
.preference(key: SizePreferenceKey.self, value: geometryProxy.size)
}
)
}
}
.frame(maxWidth: .infinity, maxHeight: 50)
.background(Color.gray)
.onPreferenceChange(SizePreferenceKey.self) { newSize in
if (freeSpace == nil) {
freeSpace = newSize
}
}
}
}
}
Credits to Federico Zanetello for the code that measures the size of the children.
As I said before, I don't see this as a very elegant solution, mainly if we compare it to the other platforms. I'd appreciate any suggestion! Just keep in mind the example I gave: fixed sized views intercalated with flexibly sized views.
Edit: I'm trying to build a component called Flexbox. In my example, I set the widths of each view, but it's just for simplicity. In fact, inside the Flexbox component, I don't know the size of any of the children, that's why, in my solution, I had to measure them.
Example: I might call:
Flexbox {
ViewWith100Width()
Flexible(flex: 2) { ViewWithFlexibleSize() }
ViewWith80Width()
Flexible(flex: 1) { AnotherViewWithFlexibleSize() }
}
But I could also call:
Flexbox {
ViewWith40Width()
Flexible(flex: 2) { ViewWithFlexibleSize() }
ViewWith60Width()
Flexible(flex: 1) { AnotherViewWithFlexibleSize() }
}
The component Flexbox must be agnostic of the size of whichever child is passed to it. That's why I had to measure each non-flexible child.
I haven't mentioned this in the original formulation of the question because I didn't want to overcomplicate it, since I just want to know if there's an easier way to simulate the Flexbox behavior of web/flutter/compose in swiftui.

