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