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() } ?

Answered by BabyJ in 726829022

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).

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