Placing important UI elements near the bottom of the screen for better accessibility

Jan 30, 2024

It has been a long held platform convention for iOS apps to use the top corners for button. These buttons are usually used for saving the current state of the UI, cancelling the current flow, or something similar. As the iPhone screens have evolved in size over the years these buttons are becoming increasingly difficult to hit, and as a consequence more and more apps have started putting these types of buttons nearer to the bottom of the screen.

Consider the following view:

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Headline text goes first").font(.largeTitle)
            Text("Some other text comes second")

            Spacer()

            Button("Continue") { print("Continue") }
                .buttonStyle(.borderedProminent)
        }
    }
}

This view solves the placement of the button. The button flows to the bottom of the screen, and respects the safe area insets at the bottom, so that it is easy to hit when holding your phone with one hand. But what if the content you are showing above the button is dynamic? Then we can potentially have content that will not fit the screen.

The easiest answer here is to turn to a List or Form, and let the button be part of the scrollable content. This solves the functional need of the content, but it might not be the best looking solution.

struct ContentView: View {
    var body: some View {
        List {
            Text("Headline text goes first").font(.largeTitle)
            Text("Some other text comes second")
            Button("Continue") { print("Continue") }
        }
    }
}

There is however another alternative. The button can still live at the bottom of the screen, and can still be scrollable when needed. To achieve this can use a VStack with a minium height, wrapped in a ScrollView. This allows the stack to expand to the full size of the screen when content is smaller than the screen, and expands beyond the size of the screen when the content is larger.

struct ContentView: View {
    var body: some View {
        GeometryReader { geometryProxy in
            ScrollView {
                VStack {
                    Text("Headline text goes first").font(.largeTitle)
                    Text("Some other text comes second")

                    ForEach(0..<40, id: \.self) { number in Text("Number \(number)") }

                    Spacer()

                    Button("Continue") { print("Continue") }
                        .buttonStyle(.borderedProminent)
                }
                .frame(maxWidth: .infinity, minHeight: geometryProxy.size.height, alignment: .top)
            }
        }
    }
}

This solution is also compatible with the safe area insets, and will also respect any keyboard presentation, and allow all of the content to still be visible and scrollable.