Equal width for views

Hi,


I am wondering if there is way in SwiftUI to make the two Text labels the same size so that the text fields align, similar to an 'Equal width' constraint in Auto Layout:


Example code:


struct FormView: View {

    @State var name : String = ""
    @State var birthDate : String = ""

    var body: some View {

        VStack {

            HStack {
                Text("Name:")
                TextField("", text: $name)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }

            HStack {
                Text("Date of birth:")
                TextField("", text: $birthDate)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
            }

            Spacer()
        }
        .padding(20)

    }

}


Screenshot:

https://ralfebert-assets.fra1.cdn.digitaloceanspaces.com/form-equal-width.png


Greetings,


Ralf

Replies

The intention here is that this is possible with the aid of custom alignments, which are discussed in WWDC 2019 Session 237. However, in this instance I'm having a little trouble taming the layout part, since it appears sizing happens before the alignment kicks in, and thus the stack views all stretch out beyond their parent view's bounds.


Here's the code so far:


extension HorizontalAlignment {
    private enum LabelTrailingEdge: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            return context[.trailing]
        }
    }
    static let labelTrailingEdge = HorizontalAlignment(LabelTrailingEdge.self)
}

struct ContentView: View {
    @State var name: String = ""
    @State var birthDate: String = ""

    var body: some View {
        VStack(alignment: .labelTrailingEdge) {
            HStack(alignment: .firstTextBaseline) {
                Text("Name:")
                    .alignmentGuide(.labelTrailingEdge) { $0[.trailing] }
                TextField("Name", text:$name)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .labelsHidden()
            }

            HStack(alignment: .firstTextBaseline) {
                Text("Date of birth:")
                    .alignmentGuide(.labelTrailingEdge) { $0[.trailing] }
                TextField("Date of birth", text:$name)
                    .textFieldStyle(RoundedBorderTextFieldStyle())
                    .labelsHidden()
            }

            Spacer()
        }
        .padding(20)
    }
}


Behind this there's a lesson on the nature of the alignment argument used by stack views. It seems reasonable to assume that `alignment: .leading` means "align my contents to my (the stack's) leading edge." However, that's not actually what it's referring to—it's actually saying "which of the alignment guides provided by my content views should I use to align them along this axis." Thus, by specifying .leading, you're telling it to position its subviews such that the leading alignment guides for each view all resolve to the same coordinate value.


Every view has some alignment guides that it vends to SwiftUI. There's usually a leading and trailing guide, top and bottom, and items containing text will vend a firstTextBaseline and lastTextBaseline. There may be many more (you can define your own, as above), and a default value is provided for any guide that isn't explicitly defined on a view (again, see the HorizontalAlignment extension above). Specifying an alignment for a stack tells the stack which of these guides to use, and it will use that one and ignore all the others.


Thus, the new alignment guide above gets selected in the outer VStack, and it uses that to align the two HStacks it contains. You provide a custom value for the alignment guide on the two labels with the .alignmentGuide() modifier, and for each one return a value matching the trailing edge of its content. The VStack then adjusts its subviews such that the two alignment guides obtain the same coordinate.


Unfortunately, something else goes on after this. It looks like the 'name' row is sliding to the right, widening the coordinate space, which causes it to redraw at that wider coordinate space. Then things are realigned once more, leading to the strange situation we have here. Still, it's a start, and I imagine a solution will turn up.

FYI: I've filed a Radar on this issue: FB7406474. Hopefully the response will be “that's expected, but here's how you really do it.”