Search code examples
swiftuilayoutpositionoffsethorizontal-alignment

In SwifUI, how do I precisely center a view and place other views relative to it horizontally?


I'm trying to place these Text views as follows

  • "App title" in the center
  • "Edit" and "..." (3-dot menu) on the far right

When I try the .position() modifier to manually center the "App title" precisely, that seems to work. However, its returned view takes up the entire available space and pushes the Text views for "Edit" and "..." completely out of the desired position.

struct SOPosition: View {
    var body: some View {
        HStack {
            Text("App title")
                .border(Color.blue)
                .position(x: UIScreen.width / 2)
                .border(Color.red)
            Group {
                Text("Edit")
                Text("...")
            }.border(.yellow)
                .frame(alignment: .trailing)
        }
        
        Spacer()
    }
} // SOPosition

Below is a screenshot for it

Layout using position() modifier

When I try to use .offset() modifier, I get approximate result by tweaking the coordinate values. Is there a cleaner way which does not involve hard-coded values.


struct SOOffset: View {
    var body: some View {
        HStack {
            Text("").frame(alignment: .leading)
            Spacer()
            Text("App title").offset(x: 40)
            Spacer()
            Group {
                Text("Edit")
                Text("...")
            }.frame(alignment: .trailing).offset(x: -20)
        }
        
        Spacer()
    }
} // SOOffset

Below is a screenshot for it

Layout using offset() modifier


Solution

  • I would suggest showing the menu items in an overlay over the title, with trailing alignment. This way, if the title and the buttons have different heights, perhaps because the title uses a bigger font, they stay aligned in the vertical center.

    When you want to have content fill the full width or height of the screen, you can either use a Spacer to hog the unused space (as you are doing), or you can use .frame(maxWidth: .infinity, maxHeight: .infinity). Both techniques are valid and sometimes you will want to use one, sometimes the other. However, two advantages of using .frame are:

    • you can apply it to any element (there is no need to nest inside another stack)
    • there are no issues of spacing between Spacer and other content (when used in a stack with non-zero spacing).

    Also, if you need the exact size of the screen then you should use a GeometryReader. This works better for split screen on iPad. UIScreen.main.bounds is deprecated anyway.

    So here is an updated version of your example:

    var body: some View {
        VStack {
            Text("App title")
                .font(.largeTitle)
                .border(Color.blue)
                .frame(maxWidth: .infinity)
                .overlay(alignment: .trailing) {
                    HStack {
                        Text("Edit")
                        Text("...")
                    }
                }
                .padding(.horizontal)
                .border(.yellow)
    
            Text("main content")
                .frame(maxWidth: .infinity, maxHeight: .infinity)
        }
    }
    

    Screenshot