implementing form behaviour in custom inputs

Hey there.

I’m trying add a custom build input (like TextField) to a Form, archiving the same behaviour like native SwiftUI fields: All labels are aligned to the right.

Is there such way to archive that with a HStack { Text("label") InputView() } ?

Accepted Answer

iOS 16

You can use the new Grid API with a custom alignment for the labels.

Grid(alignment: .leadingFirstTextBaseline) {
    GridRow {
        Text("Username:")
            .gridColumnAlignment(.trailing) // align the entire first column

        TextField("Enter username", text: $username)
    }

    GridRow {
        Label("Password:", systemImage: "lock.fill")

        SecureField("Enter password", text: $password)
    }

    GridRow {
        Color.clear
            .gridCellUnsizedAxes([.vertical, .horizontal])

        Toggle("Show password", isOn: $showingPassword)
    }
}


iOS 15 and earlier

You can achieve this through the use of custom alignment guides and a custom view that wraps up the functionality for each row.

extension HorizontalAlignment {
    private struct CentredForm: AlignmentID {
        static func defaultValue(in context: ViewDimensions) -> CGFloat {
            context[HorizontalAlignment.center]
        }
    }

    static let centredForm = Self(CentredForm.self)
}

struct Row<Label: View, Content: View> {
    private let label: Label
    private let content: Content

    init(@ViewBuilder content: () -> Content, @ViewBuilder label: () -> Label) {
        self.label = label
        self.content = content()
    }

    init(@ViewBuilder content: () -> Content) where Label == EmptyView {
        self.init(content: content) { EmptyView() } 
    }

    init(_ titleKey: LocalizedStringKey, @ViewBuilder content: () -> Content) where Label == Text {
        self.init(content: content) { Text(titleKey) } 
    }

    init<S: StringProtocol>(_ title: S, @ViewBuilder content: () -> Content) where Label == Text {
        self.init(content: content) { Text(title) }
    }

    var body: some View {
        HStack {
            label.alignmentGuide(.centredForm) { $0[.trailing] }
            content.alignmentGuide(.centredForm) { $0[.leading] }
        }
    }
}

The multiple initialisers are there for convenience and taken from the standard SwiftUI controls. Fell free to remove the ones you don't use.


It can then be implemented like this:

// need to have the alignment parameter for it to work
VStack(alignment: .centredForm) {
    // with text label
    Row("Username:") {
        TextField("Enter username", text: $username)
    }

    // with view label
    Row {
        SecureField("Enter password", text: $password)
    } label: {
        Label("Password:", systemImage: "lock.fill")
    }

    // without label but still aligned correctly
    Row {
        Toggle("Show password", isOn: $showingPassword)
    }
}



‎Obviously, place your own views in where they need to go. Both solutions will work, just choose the one you want to use (bearing in mind target version).

I get this compiler errors on your iOS 15 example.

Static method 'buildBlock' requires that 'Row<EmptyView, Toggle<Text>>' conform to 'View'
Static method 'buildBlock' requires that 'Row<Label<Text, Image>, SecureField<Text>>' conform to 'View'
Static method 'buildBlock' requires that 'Row<Text, TextField<Text>>' conform to 'View'

EDIT: Add View protocol to Row.

EDIT: The outcome does not look quite right:

implementing form behaviour in custom inputs
 
 
Q