I'm trying to achieve a right-aligned label layout. Here's an example:
Note that the labels on the left are right-aligned. It seems simple, but I haven't found a way to do it in SwiftUI in a way that can handle dynamic changes (localized labels, dynamic text, etc).
I've tried having each row as an HStack and using a custom alignment on the labels:
struct RightAligned: View {
@State var name = ""
@State var email = ""
@State var phone = ""
var body: some View {
VStack(alignment: .labelAlign) {
Row(name: "Name", value: $name)
Row(name: "Email", value: $email)
Row(name: "Phone", value: $phone)
}
.border(Color.green)
.padding(30)
}
}
struct Row: View {
let name: String
@Binding var value: String
var body: some View {
HStack {
Text("\(name):")
.alignmentGuide(.labelAlign, computeValue: { $0[.trailing] })
TextField(name, text: $value)
}
}
}
extension HorizontalAlignment {
private enum LabelAlign: AlignmentID {
static func defaultValue(in context: ViewDimensions) -> CGFloat {
context[.trailing]
}
}
static let labelAlign = HorizontalAlignment(LabelAlign.self)
}
This will align the labels, but it just shifts the HStacks so they expand outside the containing VStack:
Also tried using preferences to pass the widths of the labels up to the parent so I can make all the label frames the same size:
struct PrefsAlignment: View {
@State var labelWidth: CGFloat = 100
@State var name = ""
@State var email = ""
@State var phone = ""
var body: some View {
VStack(alignment: .leading) {
PrefsRow(name: "Name", labelWidth: $labelWidth, value: $name)
PrefsRow(name: "Email", labelWidth: $labelWidth, value: $email)
PrefsRow(name: "Phone", labelWidth: $labelWidth, value: $phone)
}
.onPreferenceChange(LabelPreferenceKey.self) { width in
self.labelWidth = width
}
}
}
struct PrefsRow: View {
let name: String
@Binding var labelWidth: CGFloat
@Binding var value: String
var body: some View {
HStack {
HStack {
Spacer()
Text("\(name):")
.background(PreferenceSetterView())
}
.frame(width: labelWidth)
TextField(name, text: $value)
.textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
struct PreferenceSetterView: View {
var body: some View {
GeometryReader { proxy in
Rectangle()
.fill(Color.clear)
.preference(key: LabelPreferenceKey.self, value: proxy.size.width)
}
}
}
struct LabelPreferenceKey: PreferenceKey {
static var defaultValue: CGFloat = 0
static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
value = max(value, nextValue())
}
}
This puts the views in a feedback loop where the preferences drive a change to the frames of the label HStacks which recursively changes the preferences and around we go until the width is driven to 0.
You could manually set the frames of the labels to the widest one, but this is fragile and wouldn't work if they change due to things like localization or dynamic type.
How in the world do you do this simple layout?