Spacer not working with Form inside a VStack

Viewed 1138

I'm trying to get a circle on top with the form content down below, right above my TabBar. I can somewhat force this by using .frame() but I'm not a big fan of that. It seems like there should be a simpler way in order to align it to the bottom.

My understanding is that Spacer() should push the form towards the bottom and leave the circle at the top, but this doesn't seem to be the case.

var body: some View {
    VStack {
        Circle().foregroundColor(.yellow).overlay(VStack {
            Text("Example")
        }).foregroundColor(.primary)
        
        Spacer()
        
        Form {
            TextField("test", text: $a)
            TextField("test2", text: $b)
        }
    }
}

3 Answers

All scrollviews(which Form has built on) and shapes(which Circle is) are greedy in layout priority. They don't have inner limitations so if there's available space whey gonna take it

Spacer is greedy too, but it has lower priority then other greedy views

That's why in your case Form and Circle are splitting available space 50% to 50%

You need to restrict both their height to make it work.

VStack {
    Circle().foregroundColor(.yellow).overlay(VStack {
        Text("Example")
    }).foregroundColor(.primary)
    .frame(height: UIScreen.main.bounds.width)
    
    Spacer()
    
    Form {
        TextField("test", text: $a)
        TextField("test2", text: $b)
    }.frame(height: 150)
}

A way to solve this, which will look good on all devices since there are no fixed sizes, is to use SwiftUI-Introspect.

You can achieve this by getting the Form's contentHeight from the underlying UITableView.

Example:

struct ContentView: View {
    @State private var a = ""
    @State private var b = ""

    @State private var contentHeight: CGFloat?

    var body: some View {
        VStack {
            Circle()
                .foregroundColor(.yellow)
                .overlay(
                    VStack {
                        Text("Example")
                    }
                )
                .foregroundColor(.primary)
                .aspectRatio(1, contentMode: .fit)

            Spacer()

            Form {
                TextField("test", text: $a)

                TextField("test2", text: $b)
            }
            .introspectTableView { tableView in
                contentHeight = tableView.contentSize.height
            }
            .frame(height: contentHeight)
        }
    }
}

Result:

Result

Forms, like Lists, expand to take all available space in the parent view. If you switch your simulator to Light Mode, you will see a light gray area behind your TextFields. That is your form.

What then happens is the Spacer() gets compressed to nothing. The easiest way to fix this is to put a .frame(height: ???) on the Spacer() will cause the spacer to take up that amount of space and push your Form down. One caveat is that it also pushes your circle up and shrinks it as well. I don't know if this will be an issue for you as this is a simple minimal, reproducible example, but if needed, you could set a .frame() on the upper view.

        VStack {
            Circle().foregroundColor(.yellow).overlay(VStack {
                Text("Example")
            }).foregroundColor(.primary)
            .frame(width: 300)
            
            Spacer()
                .frame(height: 100)
            
            Form {
                TextField("test", text: $a)
                TextField("test2", text: $b)
            }
        }
Related