111

I have an HStack:

struct BottomList: View {
    var body: some View {
        HStack() {
            ForEach(navData) { item in
                NavItem(image: item.icon, title: item.title)
            }
        }
    }
}

How do I perfectly center its content with equal spacing automatically filling the whole width?

FYI just like Bootstraps CSS class .justify-content-around

Tom
  • 3,672
  • 6
  • 20
  • 52

7 Answers7

204

The frame layout modifier, with .infinity for the maxWidth parameter can be used to achieve this, without the need for an additional Shape View.

struct ContentView: View {
    var data = ["View", "V", "View Long"]

    var body: some View {
    VStack {

        // This will be as small as possible to fit the data
        HStack {
            ForEach(data, id: \.self) { item in
                Text(item)
                    .border(Color.red)
            }
        }

        // The frame modifier allows the view to expand horizontally
        HStack {
            ForEach(data, id: \.self) { item in
                Text(item)
                    .frame(maxWidth: .infinity)
                    .border(Color.red)
            }
        }
    }
    }
}

Comparison using .frame modifier

svarrall
  • 8,545
  • 2
  • 27
  • 32
23

I inserted Spacer() after each item...but for the LAST item, do NOT add a Spacer():

struct BottomList: View {
    var body: some View {
        HStack() {
            ForEach(data) { item in
                Item(title: item.title)
                if item != data.last { // match everything but the last
                  Spacer()
                }
            }
        }
    }
}

Example list that is evenly spaced out even when item widths are different: enter image description here

(Note: The accepted answers .frame(maxWidth: .infinity) did not work for all cases: it did not work for me when it came to items that have different widths)

kgaidis
  • 14,259
  • 4
  • 79
  • 93
  • 6
    While I don't love it, using the `.frame(maxWidth: .infinity)` didn't give me the desired look as it grew the frame of each object in the for each instead of keeping them at their defined size. This added the spacing between the objects that I was looking for. – Trevor Jul 30 '21 at 07:28
18

The various *Stack types will try to shrink to the smallest size possible to contain their child views. If the child view has an ideal size, then the *Stack will not expand to fill the screen. This can be overcome by placing each child on top of a clear Rectangle in a ZStack, because a Shape will expand as much as possible. A convenient way to do this is via an extension on View:

extension View {
    func inExpandingRectangle() -> some View {
        ZStack {
            Rectangle()
                .fill(Color.clear)
            self
        }
    }
}

You can then call it like this:

struct ContentView: View {
    var data = ["View", "View", "View"]

    var body: some View {
        VStack {

            // This will be as small as possible to fit the items
            HStack {
                ForEach(data, id: \.self) { item in
                    Text(item)
                        .border(Color.red)
                }
            }

            // Each item's invisible Rectangle forces it to expand
            // The .fixedSize modifier prevents expansion in the vertical direction
            HStack {
                ForEach(data, id: \.self) { item in
                    Text(item)
                        .inExpandingRectangle()
                        .fixedSize(horizontal: false, vertical: true)
                        .border(Color.red)
                }
            }

        }
    }
}

You can adjust the spacing on the HStack as desired.

Non-expanding and expanding stacks

John M.
  • 8,892
  • 4
  • 31
  • 42
  • 5
    No need for additional shape, you only need to add .frame modifier for children views with .infinity value so it expand to fill parent width. Please check @svarrall answer – Dot Freelancer Apr 08 '20 at 12:19
12

If items are fullwidth compatible, it will be done automatically, you can wrap items between spacers to make it happen:

struct Resizable: View {
    let text: String

    var body: some View {
        HStack {
            Spacer()
            Text(text)
            Spacer()
        }
    }
}

SingleView

So you. can use it in a loop like:

HStack {
    ForEach(data, id: \.self) { item in
        Resizable(text: item)
    }
}

MultiView

Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • 1
    An improvement on this could be: ``` struct Resizable: View { let content: () -> Content var body: some View { HStack { Spacer() content() Spacer() } } } ``` – Ugo Jan 27 '22 at 13:53
6

You can also use spacing in stacks ... ie

HStack(spacing: 30){
            
            Image("NetflixLogo")
                .resizable()
                .scaledToFit()
                .frame(width: 40)
            
            Text("TV Show")
            Text("Movies")
            Text("My List")
            
        }
.frame(maxWidth: .infinity)

output result looks like this ...

enter image description here

Wahab Khan Jadon
  • 875
  • 13
  • 21
4

If your array has repeating values, use array.indices to omit a spacer after the last element.

HStack() {
  ForEach(data.indices) { i in
    Text("\(data[i])")
    if i != data.last {
       Spacer()
    }
  }
}
al00p
  • 51
  • 3
2

I am not satisfied with the accepted answers, because they leave extra "pseudo-padding" on the leading and trailing sides of the Hstack

This is an example of my solution:

struct EvenlySpacedHStack: View {
    let items: [Int] = [1,2,3]
    var body: some View {
        ZStack() {
            Capsule().foregroundColor(.yellow)
            HStack() {
                ForEach(Array(items.enumerated()), id: \.element) { index, element in
                    
                    switch index {
                    case items.count - 1:
                        Image(systemName: "\(element).circle.fill")
                            .resizable()
                            .aspectRatio(1.0, contentMode: .fit)
                    default:
                        Image(systemName: "\(element).circle.fill")
                            .resizable()
                            .aspectRatio(1.0, contentMode: .fit)
                        Spacer()
                    }
                }
            }
            .padding(8)
        }
        .frame(height: 55)
    }
}

And results in this:

enter image description here

Rillieux
  • 587
  • 9
  • 23