HStack fill whole width with equal spacing
Asked Answered
T

9

126

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

Tierratiersten answered 16/11, 2019 at 4:19 Comment(0)
D
227

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

Diplo answered 10/2, 2020 at 6:49 Comment(1)
When used with an Image, this adds extra horizontal padding to each image, thus causing misalignment. Cheers.Annamarieannamese
L
29

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)

Legwork answered 15/1, 2021 at 3:17 Comment(1)
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.Rocker
M
19

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

Malinda answered 16/11, 2019 at 6:12 Comment(1)
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 @Diplo answerChazan
F
14

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

Forelock answered 4/9, 2020 at 7:42 Comment(1)
An improvement on this could be: ``` struct Resizable<Content: View>: View { let content: () -> Content var body: some View { HStack { Spacer() content() Spacer() } } } ```Cantara
S
9

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

Subcontract answered 19/1, 2021 at 11:7 Comment(0)
C
6

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()
    }
  }
}
Cockaleekie answered 21/5, 2021 at 17:46 Comment(0)
P
4

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

Palaver answered 10/6, 2023 at 10:20 Comment(0)
S
1

Problem With The Provided Answers

Since the provided answers work well in various situations, none of them creates the true space-between spacing behavior similar to CSS, where all elements are equally spaced regardless of the container width.

Using just a plain Spacer between elements may cause elements and text to be shrunk and truncated in smaller containers when there isn't enough space for both the elements and the Spacers, as the Spacer also requires some amount of space.

The results of the provided answers may appear like this:

image of truncated elements

Solution

However, you can resolve this issue by setting the minimumWidth for the Spacers to 0 and assigning a higher layoutPriority to the desired elements like this:

struct SpaceBetweenView: View {
    
    var body: some View {
        HStack(spacing: 0) {
            ForEach(0..<6, id: \.self) { item in
                if item != 0 {
                    Spacer()
                    // Set the minWidth to 0 so the Spacer can shrink to 0 width
                    // and doesn't block any space required for the Text view below.
                        .frame(minWidth: 0) // <--
                }
                
                Text("Item \(item)")
                    .lineLimit(1)
                    .background(Color.green)
                // Use a higher layoutPriority for the desired view to prevent it from shrinking
                // when there is not enough space in the container
                // for both (the Spacers and the elements).
                    .layoutPriority(1) // <--
            }
        }
        .frame(width: 300, height: 200)
        .border(Color.black)
    }
}

#Preview {
    SpaceBetweenView()
}

By this technique, you can achieve true horizontal space-between behavior for your views regardless of the container size. The text elements will no longer be truncated due to the use of Spacers, and will only truncate when there isn't enough space for the elements themselves.

Result

image of properly spaced elements

Tested on Xcode 15.2, Swift 5, iOS 16.6

Sol answered 15/2 at 15:1 Comment(1)
This is the most accurate answer.Oliveolivegreen
U
0

you just have to add the .frame(maxWidth: .infinity) modifier to each element

struct BottomList: View {
    var body: some View {
        HStack() {
            ForEach(navData) { item in
                NavItem(image: item.icon, title: item.title)
                   .frame(maxWidth: .infinity)
            }
        }
    }
}
Uninhibited answered 20/4 at 1:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.