Alexito's World

A world of coding 💻, by Alejandro Martinez

Trying to understand custom layouts in SwiftUI

One of the things that I still don't fully understand about SwiftUI is how to customise the layout of views.

HStack and VStack are fine for the majority of cases, but there is one case that bothers me that is not as easy as it should.

This is how a HStack with 3 views looks like:

HStack() {
    ForEach(1...3) { _ in
        Color.red.frame(width: 50.0, height: 50)
    }
}

The 3 views are centred and with a default spacing. You can change the alignment if you need to, but you don't have any options for what UIStackView called "distribution".

What if you want it not to be centred but with equal spacings?

This is totally doable by using the Spacer view. The problem is that is a little cumbersome.

HStack {
    Spacer()
    ForEach(1...3) { _ in
        Color.red.frame(width: 50, height: 50)
        Spacer()
    }
}

Of course you can extract that into a function that given some data it calls a given closure.

func EquallySpacedHStack<Data, Content>(_ data: Data, content: @escaping (Data.Element.IdentifiedValue) -> Content) -> some View where Data : RandomAccessCollection, Content : View, Data.Element : Identifiable  {
    
    HStack {
        Spacer()
        ForEach(data) { item in
            content(item)
            Spacer()
        }
    }
}

Now you can create it like if it was a ForEach:

EquallySpacedHStack(1...3) { _ in
    Color.red.frame(width: 50, height: 50)
}

And that's something I don't like. I don't want to have to pass the data in and be limited to a for each like structure. I want it to be able to use it as any other stack.

I've tried to replicate it with a custom view that uses the @ViewBuilder in the initialiser, and is better:

struct EquallySpacedHStack2<Items: View>: View {
    let items: [AnyView]
    
    init<Data, Content: View>(@ViewBuilder content: () -> Items) where Items == ForEach<Data, Content> {
        let views = content()
        self.items = views.data.map({ AnyView(views.content($0.identifiedValue)) })
    }
    
    var body: some View {
        HStack {
            Spacer()
            ForEach(0..<items.count) { index in
                self.items[index]
                Spacer()
            }
        }
    }
}

but I'm also forced to use a ForEach:

EquallySpacedHStack2 {
    ForEach(1...3) {
        Color.red.frame(width: 50, height: 50)
    }
}

so it's not that much better.

The missing piece here is to be able to get each individual subview and layout it individually. So I'm free to use a ForEach or specifying one by one the views that I want.

EquallySpacedHStack {
    Color.red.frame(width: 50, height: 50)
    Color.blue.frame(width: 50, height: 50)
    Color.green.frame(width: 50, height: 50)
}

But I still haven't figured it out yet ^^

I'm sure there is some lower level part of the layout system that could be used, but the documentation right now is quite poor and I haven't found anything yet.

I will keep looking :)

If you liked this article please consider supporting me