SwiftUI TupleView and equally spaced stacks
Yesterday I shared with you a post where I was trying to understand SwiftUI’s layout system in order to implement equally spaced distribution for views inside stacks.
I was able to do it by requiring a ForEach
but I was not happy as that’s not how HStack
works. SwiftUI stacks allow you to nest views individually or use a ForEach
. I wanted to support both systems also.
Then, while editing the video about Opaque Result Types, I realised how HStack
keeps the information of its views in the type thanks to TupleView
.
TupleView
is a generic view with a tuple as its type. This allows it to keep the information of every single subview. Then I remembered some discussions on the forums about Swift not having, yet, variadic generics, and a possible solution came to my mind.
The trick is to make different init
with @ViewBuilder
that are overloaded on the return type of the closure.
init<A: View, B: View>(@ViewBuilder content: () -> TupleView<(A, B)>)
init<A: View, B: View, C: View>(@ViewBuilder content: () -> TupleView<(A, B, C)>)
init<A: View, B: View, C: View, D: View>(@ViewBuilder content: () -> TupleView<(A, B, C, D)>)
...
You can see the pattern.
This allows me to get access to the specific subviews of the stack, store them in an array and then add a Spacer
between them in the body, in the same way that was done in the previous post.
But this journey doesn’t end here. There are still a couple of things that I don’t like.
To support a single subview overloading the init with a tuple of 1 element doesn’t work, the TupleView
needs to disappear:
// This doesnt' allow for a single subview:
init<A: View>(@ViewBuilder content: () -> TupleView<(A)>)
// It needs this overload instead:
init<A: View>(@ViewBuilder content: () -> A)
The problem with having that overload is that it’s called when there are a number of subviews that we don’t have another init for, so in that init we receive a TupleView and we treat it as a single view without adding spacing. Too fragile.
The other problem is that in order to gather the views they need to be wrapped in AnyView
, a type erased container. (Watch Opaque Result Types to know more about this view)
So this is still not the ideal solution implementation wise, but at the API surface is what I wanted:
Text("With reusable view manual")
EquallySpacedHStack {
Color.red.frame(width: 50, height: 50)
Color.blue.frame(width: 50, height: 50)
Color.green.frame(width: 50, height: 50)
}
Text("With reusable view for each")
EquallySpacedHStack {
ForEach(1...4) { _ in
Color.red.frame(width: 50, height: 50)
}
}
Check the GitHub repo.