Correct way to label TextField inside Form in SwiftUI

Hello everyone. I'm building a simple Form in a Multiplatform App with SwiftUI. Originally I had something like this.

import SwiftUI

struct OnboardingForm: View {
    @State var firstName: String = ""
    @State var lastName: String = ""
    @State var email: String = ""
    @State var job: String = ""
    @State var role: String = ""
    
    var body: some View {
        Form {
            TextField("First Name", text: $firstName, prompt: Text("Required"))
            TextField("Last Name", text: $lastName, prompt: Text("Required"))
            TextField("Email", text: $email, prompt: Text("Required"))
            TextField("Job", text: $job, prompt: Text("Required"))
            TextField("Role", text: $role, prompt: Text("Required"))
        }
    }
}

#Preview {
    OnboardingForm()
}

In macOS it looks ok but then in iOS it looks like this:

and it's impossible to know what each field is for if all the prompts are the same. I tried adding LabeledContent around each text field and that solves it for iOS but then on macOS it looks like this:

The labels are shown twice and the columns are out of alignment. I think I could get around it by doing something like this:

#if os(iOS)
            LabeledContent {
                TextField("First Name", text: $firstName, prompt: Text("Required"))
            } label: {
                Text("First Name")
            }
#else
            TextField("First Name", text: $firstName, prompt: Text("Required"))
#endif

but it seems to me like reinventing the wheel. Is there a "correct" way to declare TextFields with labels that works for both iOS and macOS?

Answered by DTS Engineer in 823956022

@alexortizl You can use a LabeledContent and create a customLabeledContentStyle that allows you customize the appearance.

struct CustomTextField: View {
    let label: String
    let prompt: String
    @Binding var text: String
    
    var body: some View {
        LabeledContent(label) {
            TextField(label, text: $text, prompt: Text(prompt))
        }
        .labeledContentStyle(.customStyle)
    }
}

struct CustomLabeledContentStyle: LabeledContentStyle {
    func makeBody(configuration: Configuration) -> some View {
        #if os(macOS)
        configuration.content
        #else
        HStack {
            configuration.label
            configuration.content
        }
        #endif
    }
}

extension LabeledContentStyle where Self == CustomLabeledContentStyle {
    static var customStyle: CustomLabeledContentStyle { CustomLabeledContentStyle() }
}

 Form {
            CustomTextField(label: "First Name", prompt: "Required", text: $firstName)
            CustomTextField(label: "Last Name", prompt: "Required", text: $lastName)
            CustomTextField(label: "Email", prompt: "Required", text: $email)
            CustomTextField(label: "Job", prompt: "Required", text: $job)
            CustomTextField(label: "Role", prompt: "Required", text: $role)
        }

I've never needed to do this, but I've had a quick look, and this seems reasonable:

	var body: some View {
		VStack(alignment: .leading) {
			Form {
				Text("First Name")
				TextField("", text: $firstName, prompt: Text("Required"))
				Text("Last Name")
					.padding(.top, 10)
				TextField("", text: $lastName, prompt: Text("Required"))
				Text("Email")
					.padding(.top, 10)
				TextField("", text: $email, prompt: Text("Required"))
				Text("Job")
					.padding(.top, 10)
				TextField("", text: $job, prompt: Text("Required"))
				Text("Role")
					.padding(.top, 10)
				TextField("", text: $role, prompt: Text("Required"))
			}
			.formStyle(.columns)
			.padding(.horizontal, 16)
			.padding(.vertical, 16)
			Spacer()
		}
	}

The key is .formStyle(.columns).

Accepted Answer

@alexortizl You can use a LabeledContent and create a customLabeledContentStyle that allows you customize the appearance.

struct CustomTextField: View {
    let label: String
    let prompt: String
    @Binding var text: String
    
    var body: some View {
        LabeledContent(label) {
            TextField(label, text: $text, prompt: Text(prompt))
        }
        .labeledContentStyle(.customStyle)
    }
}

struct CustomLabeledContentStyle: LabeledContentStyle {
    func makeBody(configuration: Configuration) -> some View {
        #if os(macOS)
        configuration.content
        #else
        HStack {
            configuration.label
            configuration.content
        }
        #endif
    }
}

extension LabeledContentStyle where Self == CustomLabeledContentStyle {
    static var customStyle: CustomLabeledContentStyle { CustomLabeledContentStyle() }
}

 Form {
            CustomTextField(label: "First Name", prompt: "Required", text: $firstName)
            CustomTextField(label: "Last Name", prompt: "Required", text: $lastName)
            CustomTextField(label: "Email", prompt: "Required", text: $email)
            CustomTextField(label: "Job", prompt: "Required", text: $job)
            CustomTextField(label: "Role", prompt: "Required", text: $role)
        }
Correct way to label TextField inside Form in SwiftUI
 
 
Q