How to position a button in lower right of screen in SwiftUI

70 Views Asked by At

Consider the following view:

var body: some View {
    
    GeometryReader { geo in
        
        ZStack {
            HStack(spacing:  0) {
                VStack {
                    Text("The number two")
                    Text("2")
                        .font(.largeTitle)
                }
                .frame(maxWidth: .infinity)
                .background(Color.purple)
                
                VStack {
                    Text("The number eight")
                    Text("8")
                        .font(.largeTitle)
                }
                .frame(maxWidth: .infinity)
                .background(Color.green)
                
            }
            
            Button(action: {
                print("Button tapped!")
            }) {
                Text("NEXT")
            }
            .padding()
            .background(Color.orange)
            .foregroundColor(.white)
            .clipShape(RoundedRectangle(cornerRadius:  10))
            .position(x:geo.size.width, y:geo.size.height)
            .padding(16)
            
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.gray)
        
    }
    
}

On all devices, I want the button positioned in the lower right of the screen, 16 pixels from the trailing edge, and 16 pixels from the bottom edge, regardless of what else is on the screen (in the ZStack). What is the best practice to achieve that? Here's what it looks like now:

enter image description here enter image description here

2

There are 2 best solutions below

4
jnpdx On BEST ANSWER

Your current code is behaving the way it is because position in SwiftUI positions the center of the View. But, you don't actually need position or GeometryReader to do this.

You can assign a frame to the orange button and align it to bottomTrailing:

struct ContentView: View {
    var body: some View {
        ZStack {
            HStack(spacing:  0) {
                VStack {
                    Text("The number two")
                    Text("2")
                        .font(.largeTitle)
                }
                .frame(maxWidth: .infinity)
                .background(Color.purple)
                
                VStack {
                    Text("The number eight")
                    Text("8")
                        .font(.largeTitle)
                }
                .frame(maxWidth: .infinity)
                .background(Color.green)
            }
            
            Button(action: {
                print("Button tapped!")
            }) {
                Text("NEXT")
            }
            .padding()
            .background(Color.orange)
            .foregroundColor(.white)
            .clipShape(RoundedRectangle(cornerRadius:  10))
            .padding(16)
            .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) // <-- Here
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.gray)
    }
}
1
Benzy Neez On

Your ZStack is already occupying the full screen by virtue of the frame with maxWidth and maxHeight. So the button can be shown as an overlay in the bottom trailing corner. If fact, if the button is shown as an overlay then you don't need the ZStack at all, you can promote the HStack to top-level instead.

However, the top-level parent (be it ZStack or HStack) is currently not ignoring the safe areas. This means, if there are insets at the bottom or side of the screen then the distance to the button will be (insets + 16). This is probably more than you want.

Assuming you don't want to ignore the safe area insets with the parent container, one way to move the button closer to the edges would be to set offsets equal to the size of the insets. The GeometryReader gives you these:

.offset(
    x: geo.safeAreaInsets.trailing,
    y: geo.safeAreaInsets.bottom
)

I would suggest, a better approach is to apply padding only where it is needed. If the insets already exceed the padding, then no more padding is needed.

var body: some View {
    GeometryReader { geo in
        HStack(spacing:  0) {
            // content as before
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .overlay(alignment: .bottomTrailing) {
            Button("NEXT") {
                print("Button tapped!")
            }
            .padding()
            .background(.orange)
            .foregroundStyle(.white)
            .clipShape(RoundedRectangle(cornerRadius:  10))
            .padding(.trailing, max(0, 16 - geo.safeAreaInsets.trailing))
            .padding(.bottom, max(0, 16 - geo.safeAreaInsets.bottom))
        }
        .background(.gray)
    }
}

Portrait

Landscape