53

How can I center horizontally a View (Image) in an HStack? I want a button to be left aligned and the image to be centered horizontally the view.

Currently I have this structure:

VStack {
        HStack {
            Button(action: {
                print("Tapped")
            }, label: {
                Image("left-arrow")
                    .resizable()
                    .frame(width: 30, height: 30, alignment: .leading)
            }).padding(.leading, 20)
            
            Spacer()
            
            Image("twitter-logo")
                .resizable()
                .frame(width: 30, height: 30, alignment: .center)
            
        }
        Spacer()
    }

Which is giving me this:

enter image description here

But I want to achieve this:

enter image description here

SwiftiSwift
  • 7,528
  • 9
  • 56
  • 96
  • 2
    Don't you need another `Spacer()` in the `HStack` after the `Image`? – ielyamani Jun 05 '19 at 15:00
  • Example of Stack : https://iosdevcenters.blogspot.com/2019/09/swiftui-whats-different-between-vstack.html – Kirit Modi Sep 26 '19 at 03:42
  • @ ielyamani - no, this wouldn't work in this case because of his padding. Or he would add a padding on the right side too... – Chris Dec 04 '19 at 07:07

9 Answers9

67

You can embed two HStack's in a ZStack and place spacers accordingly for the horizontal spacing. Embed all that in a VStack with a Spacer() to have everything pushed up to the top.

struct ContentView : View {
    var buttonSize: Length = 30
    var body: some View {
        VStack {
            ZStack {
                HStack {
                    Button(action: {
                        
                    }, label: {
                        Image(systemName: "star")
                            .resizable()
                            .frame(width: CGFloat(30), height: CGFloat(30), alignment: .leading)
                    }).padding(.leading, CGFloat(20))
                    
                    Spacer()
                }
                
                HStack {
                    Image(systemName: "star")
                        .resizable()
                        .frame(width: CGFloat(30), height: CGFloat(30), alignment: .center)
                }
            }
            Spacer()
        }
    }
}

Note: In the second HStack, the image should automatically be center aligned, but if it isn't, you can place a Spacer() before and after the image.

Edit: Added the VStack and Spacer() to move everything to the top like the OP wanted.

Edit 2: Removed padding on image because it caused the image to be slightly offset from the center. Since it is in its own HStack and center-aligned, it does not need padding.

Edit 3: Thanks to @Chris Prince in the comments, I decided to make a simple NavigationBar-esque custom view that you can provide left, center, and right arguments to create the effect that the OP desired (where each set of views are aligned independently of each other):

struct CustomNavBar<Left, Center, Right>: View where Left: View, Center: View, Right: View {
    let left: () -> Left
    let center: () -> Center
    let right: () -> Right
    init(@ViewBuilder left: @escaping () -> Left, @ViewBuilder center: @escaping () -> Center, @ViewBuilder right: @escaping () -> Right) {
        self.left = left
        self.center = center
        self.right = right
    }
    var body: some View {
        ZStack {
            HStack {
                left()
                Spacer()
            }
            center()
            HStack {
                Spacer()
                right()
            }
        }
    }
}

Usage:

struct ContentView: View {
    let buttonSize: CGFloat = 30
    var body: some View {
        VStack {
            CustomNavBar(left: {
                Button(action: {
                    print("Tapped")
                }, label: {
                    Image(systemName: "star")
                        .resizable()
                        .frame(width: self.buttonSize, height: self.buttonSize, alignment: .leading)
                }).padding()
            }, center: {
                Image(systemName: "star")
                    .resizable()
                    .frame(width: 30, height: 30, alignment: .center)
            }, right: {
                HStack {
                    Text("Long text here")
                    Image(systemName: "star")
                        .resizable()
                        .frame(width: 30, height: 30, alignment: .center)
                        .padding(.trailing)
                }.foregroundColor(.red)
            })
            Spacer()
            Text("Normal Content")
            Spacer()
        }
    }
}

Example code shown on simulator

pawello2222
  • 46,897
  • 22
  • 145
  • 209
RPatel99
  • 7,448
  • 2
  • 37
  • 45
  • 1
    Why should I use your answer when the one above is simpler and working fine. I don't understand the ZStack thing and what it is etc – SwiftiSwift Jun 06 '19 at 09:19
  • 1
    @jsbeginnerNodeJS You can use whichever answer you want, but my answer is a better design (imo). If you ever decide to add another view (button, image, etc) to the `HBox` in the other answer, it will mess up your layout. With my answer, you could add anything to the top without messing up the button and image's layout. Since the button and image are not positioned **relative** to each other, it is better to separate them into different containers, because the different kind of `Stack`s are meant to position their children views relative to each other. – RPatel99 Jun 06 '19 at 16:29
  • Also, `ZStack`s order children in the Z direction, like how `HStack`s order in the x direction and `VStack`s in the y direction. It allows you to overlay mulitple views on top of each other. From the Wikipedia article for Z-order, "An analogy would be some sheets of paper scattered on top of a table, each sheet being a window [view, in our case], the table your computer [apple device, in our case] screen, and the top sheet having the highest Z value." – RPatel99 Jun 06 '19 at 16:32
  • While I've got this working now, using these suggestions, these seem non-obvious for something that seems like a pretty common use case. Ughh. – Chris Prince Oct 21 '19 at 03:16
  • @ChrisPrince You could make your own NavigationBar-esque custom view that takes independent left, right, and center aligned `@ViewBuilder` closures -- that's one of the great things about SwiftUI's modularity. Check my edit (I decided to make it). – RPatel99 Oct 21 '19 at 06:19
  • I have a question. Let's say the center of the nav bar is a dynamic text, that can be quite long at times. Is there a way to constraint the text width so it doesn't overlap the lower right and left views? It seems that in ZStack there's no way to achieve that. – Yoni Reiss Jan 01 '22 at 19:07
27

What's about saving button size to a property and add a negative padding to the image? And pay attention to an additional spacer after the image.

struct ContentView: View {

    var buttonSize: Length = 30

    var body: some View {
        VStack {
            HStack {
                Button(action: {
                    print("Tapped")
                }, label: {
                    Image(systemName: "star")
                        .resizable()
                        .frame(width: buttonSize, height: buttonSize, alignment: .leading)
                })
                Spacer()
                Image(systemName: "star")
                    .resizable()
                    .frame(width: 30, height: 30, alignment: .center)
                    .padding(.leading, -buttonSize)
                Spacer()
            }
            Spacer()
        }
    }
}

The result:

enter image description here

Artem Novichkov
  • 2,356
  • 2
  • 24
  • 34
  • 1
    Use a `ZStack` instead of a `VStack` to avoid the extra padding stuff (see my answer). – RPatel99 Jun 05 '19 at 15:34
  • It works with a little bit of modification but why? I don't why – SwiftiSwift Jun 05 '19 at 18:03
  • @jsbeginnerNodeJS VStack puts the button vertically above the image, and the negative padding offsets that vertical displacement on the image. It’s a “hacky” way to do it. My answer uses ZStack, which doesn’t require the offset because of the two HStacks are overlayed on top of each other in the Z direction. – RPatel99 Jun 05 '19 at 19:34
  • @RPatel99 your answer wasn't working. It wasn't centered. I asked artem. – SwiftiSwift Jun 05 '19 at 20:33
  • @jsbeginnerNodeJS Sorry, I was under the impression that this answer was placing the image as another item in the VStack so ignore my previous comment. The padding offset just moves the button to the left, but its still a "hacky" way of doing it. Why wasn't my answer working? It worked when I tested it. I edited it to include every bit of code that I used to test it, so if you could let me know why it doesn't work for you, that'd be great. – RPatel99 Jun 05 '19 at 21:18
  • @RPatel99 see question again. – SwiftiSwift Jun 05 '19 at 22:58
  • @jsbeginnerNodeJS See my updated answer and the Edit 2. – RPatel99 Jun 05 '19 at 23:03
13

Easiest way for me:

ZStack(){
    HStack{
        Image("star").resizable().foregroundColor(.white).frame(width: 50, height: 50)
        Spacer()
    }
        Image("star").resizable().font(.title).foregroundColor(.white).frame(width: 50, height: 50)
}
DaWiseguy
  • 786
  • 7
  • 16
8

You center the view using position property try this code

Group{ // container View
   Image("twitter-logo")
            .resizable()
            .frame(width: 30, height: 30, alignment: .center)
}.position(x: UIScreen.main.bounds.width/2)
kishor soneji
  • 795
  • 1
  • 9
  • 16
  • 1
    Works unless your app is used side by side on iPad with another App. GeometryReader is more reliable. – Darko Feb 03 '22 at 07:56
5

the right way to center the Title like navigationbar:

HStack {

    Spacer()
        .overlay {
            HStack {
                Image(systemName: "star")
                Spacer()
            }
        }

    Text("Title")
             
    Spacer()
        .overlay {
            HStack {
                Spacer()
                Image(systemName: "star")
            }
        }
}
user3172189
  • 61
  • 1
  • 2
4

Here is what worked for me

HStack {
    Image(systemName: "star.fill")
       .frame(maxWidth: .infinity, alignment: .leading)
    Image(systemName: "star.fill")
       .frame(maxWidth: .infinity, alignment: .center)
    Text("")
       .frame(maxWidth: .infinity, alignment: .trailing)
}
.foregroundColor(.yellow)

Inspired by SwiftUI - How to align elements in left, center, and right within HStack?

cora
  • 1,916
  • 1
  • 10
  • 18
2

You can place the view that you want to center into a VStack and then set the alignment to center. Make sure that you also set the frame(maxWidth: .infinity) or else it will be centering your view in the VStack but the VStack might not take up the entire width of the screen so you might not get the appearance you are trying to achieve.

To make it even easier, write it as a function that extends the View object

extension View {
    func centerInParentView() -> some View {
        VStack(alignment: .center) {
            self
        }
        .frame(maxWidth: .infinity)
    }
}

And then you can just call it as you would a view modifier i.e.

VStack {
        HStack {
            Button(action: {
                print("Tapped")
            }, label: {
                Image("left-arrow")
                    .resizable()
                    .frame(width: 30, height: 30, alignment: .leading)
            }).padding(.leading, 20)
            
            Spacer()
            
            Image("twitter-logo")
                .resizable()
                .frame(width: 30, height: 30, alignment: .center)
            
        }
        Spacer()
}
.centerInParentView()

Works every time for me

Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
NSSpeedForce
  • 127
  • 9
0

I have got an alternative solution. I used a hidden Image as placeholder.

HStack {
    Image("left-arrow").padding()
    Spacer()
    Image("twitter-logo")
    Spacer()
    // placeholder to keep layout symmetric
    Image("left-arrow").padding().hidden()
}

Of course you can replace the Images with Buttons or other Views as you prefer.

Laufwunder
  • 773
  • 10
  • 21
-1

Let me propose a different solution:

https://gist.github.com/buscarini/122516641cd0ee275dd367786ff2a736

It can be used like this:

HStack {
    Color.red
        .frame(width: 0, height: 50)
        .layoutPriority(1)
        
    GlobalHCenteringView {
        Text("Hello, world!")
            .lineLimit(1)
            .background(Color.green)
    }
    .background(Color.yellow)
    
    Color.red
        .frame(width: 180, height: 50)
        .layoutPriority(1)
    }
}

This will center the child view in the screen if it fits, or leave it as is if it doesn't. It is currently using UIScreen, so it only works on iOS, but you could easily pass the screen or parent width to the constructor of the view, getting it from a GeometryReader or whatever.

José Manuel Sánchez
  • 5,215
  • 2
  • 31
  • 24