Skip to content

Text Fields and Inputs

Daniel S edited this page Jul 2, 2026 · 1 revision

Text Fields and Inputs

DesignFoundation ships seven input primitives — DFTextField, DFSecureField, DFToggle, DFSlider, DFPicker, DFDatePicker, and DFCheckbox — each following the same style protocol pattern that governs the rest of the library. Every component reads the active theme from the environment, responds to isEnabled, and exposes a dfXxxStyle(_:) modifier for swapping visual variants.

Related pages: Getting-Started · Theming · Buttons · Forms-and-Validation


DFTextField

DFTextField is the standard single-line text input. It supports an optional label, a placeholder, leading and trailing accessory views, and first-class integration with DFValidationState.

Initializer Signatures

// 1. Plain
public init(
    _ label: String,
    text: Binding<String>,
    placeholder: String = "",
    validationState: DFValidationState = .none
)

// 2. Leading + trailing accessory views
public init<Leading: View, Trailing: View>(
    _ label: String,
    text: Binding<String>,
    placeholder: String = "",
    validationState: DFValidationState = .none,
    @ViewBuilder leading: () -> Leading,
    @ViewBuilder trailing: () -> Trailing
)

// 3. Leading accessory view only
public init<Leading: View>(
    _ label: String,
    text: Binding<String>,
    placeholder: String = "",
    validationState: DFValidationState = .none,
    @ViewBuilder leading: () -> Leading
)

// 4. Trailing accessory view only
public init<Trailing: View>(
    _ label: String,
    text: Binding<String>,
    placeholder: String = "",
    validationState: DFValidationState = .none,
    @ViewBuilder trailing: () -> Trailing
)

Style Variants

Style Token Default
DFOutlinedTextFieldStyle .outlined Yes
DFFilledTextFieldStyle .filled No
DFGlassTextFieldStyle .glass No — requires iOS 26+ / macOS 26+
// Applied to a container — all child fields inherit
VStack {
    DFTextField("First name", text: $first)
    DFTextField("Last name", text: $last)
}
.dfTextFieldStyle(.outlined)

Examples

// Minimal
DFTextField("Username", text: $username)

// With placeholder and leading icon
DFTextField("Email address", text: $email, placeholder: "you@example.com") {
    Image(systemName: "envelope")
}

// Email field with leading icon and trailing clear button
DFTextField(
    "Email",
    text: $email,
    placeholder: "you@example.com"
) {
    Image(systemName: "envelope")
} trailing: {
    if !email.isEmpty {
        Button { email = "" } label: {
            Image(systemName: "xmark.circle.fill")
        }
        .buttonStyle(.plain)
    }
}

// With live validation state
DFTextField(
    "Email",
    text: $email,
    placeholder: "you@example.com",
    validationState: emailValidationState
) {
    Image(systemName: "envelope")
}

// Filled style
VStack(spacing: 16) {
    DFTextField("Display name", text: $displayName)
    DFTextField("Handle", text: $handle, placeholder: "@username")
}
.dfTextFieldStyle(.filled)

DFSecureField

DFSecureField wraps SwiftUI's SecureField and adds a built-in show/hide toggle button on the trailing edge.

Initializer Signature

public init(
    _ label: String,
    text: Binding<String>,
    placeholder: String = "",
    validationState: DFValidationState = .none
)

Style Variants

Style Token Default
DFOutlinedSecureFieldStyle .outlined Yes
DFFilledSecureFieldStyle .filled No
DFGlassSecureFieldStyle .glass No — requires iOS 26+ / macOS 26+

Examples

// Minimal password entry
DFSecureField("Password", text: $password, placeholder: "Enter password")

// Confirm password with match validation
DFSecureField(
    "Confirm password",
    text: $confirmPassword,
    placeholder: "Repeat password",
    validationState: confirmState
)

// Filled style
VStack(spacing: 12) {
    DFTextField("Email", text: $email, placeholder: "you@example.com") {
        Image(systemName: "envelope")
    }
    DFSecureField("Password", text: $password, placeholder: "Password")
    DFButton("Sign in") { signIn() }
}
.dfTextFieldStyle(.filled)
.dfSecureFieldStyle(.filled)

DFToggle

A themed wrapper around SwiftUI's native Toggle.

Initializer Signature

public init(_ label: String, isOn: Binding<Bool>)

Style Variants

Style Token Default Description
DFSwitchToggleStyle .switch Yes Wraps native Toggle with .toggleStyle(.switch) and theme.colors.primary tint.
DFCheckboxToggleStyle .checkbox No Custom 20×20 pt rounded square checkbox with a bold checkmark.
DFGlassToggleStyle .glass No .regularMaterial checkbox; requires iOS 26+ / macOS 26+.

Examples

// Default switch
DFToggle("Enable notifications", isOn: $notificationsEnabled)

// Checkbox style
DFToggle("I agree to the terms of service", isOn: $agreed)
    .dfToggleStyle(.checkbox)

// Apply one style to a group
VStack {
    DFToggle("Marketing emails", isOn: $marketing)
    DFToggle("Product updates", isOn: $updates)
    DFToggle("Security alerts", isOn: $security)
}
.dfToggleStyle(.checkbox)

// Disabled
DFToggle("Feature flag (read-only)", isOn: .constant(true))
    .disabled(true)

DFSlider

A themed slider that wraps SwiftUI's Slider.

Initializer Signature

public init(
    _ label: String? = nil,
    value: Binding<Double>,
    in range: ClosedRange<Double> = 0...1,
    step: Double? = nil
)

Style Variants

Style Token Default Description
DFStandardSliderStyle .standard Yes Label above track; no value or end-cap labels.
DFLabeledSliderStyle .labeled No Label + current value; min/max captions below track.
DFGlassSliderStyle .glass No Labeled layout inside .regularMaterial; requires iOS 26+.

Examples

// Minimal
DFSlider(value: $opacity)

// With label
DFSlider("Volume", value: $volume, in: 0...100)

// Stepped
DFSlider("Quality", value: $quality, in: 1...5, step: 1)

// Labeled style
DFSlider("Font size", value: $fontSize, in: 10...36, step: 1)
    .dfSliderStyle(.labeled)

DFPicker

A themed picker that wraps SwiftUI's Picker. Works with any Hashable & Sendable selection type.

Initializer Signature

public init(
    _ label: String,
    selection: Binding<SelectionValue>,
    @ViewBuilder content: () -> Content
) where SelectionValue: Hashable & Sendable, Content: View

Style Variants

Style Token Default Description
DFMenuPickerStyle .menu Yes Dropdown menu on all platforms. Tinted with theme.colors.primary.
DFSegmentedPickerStyle .segmented No Horizontal segmented control. Native appearance.
DFWheelPickerStyle .wheel No Wheel on iOS/watchOS; falls back to .menu on macOS.
DFGlassPickerStyle .glass No .menu style in .regularMaterial; requires iOS 26+.

Examples

enum Role: String, CaseIterable, Hashable {
    case viewer, editor, admin
    var displayName: String { rawValue.capitalized }
}

@State private var role = Role.viewer

DFPicker("Role", selection: $role) {
    ForEach(Role.allCases, id: \.self) {
        Text($0.displayName).tag($0)
    }
}

// Segmented style
DFPicker("Sort", selection: $sort) {
    ForEach(SortOrder.allCases, id: \.self) {
        Text($0.rawValue.capitalized).tag($0)
    }
}
.dfPickerStyle(.segmented)

DFDatePicker

A themed date picker that wraps SwiftUI's DatePicker.

Initializer Signature

public init(
    _ label: String,
    selection: Binding<Date>,
    in dateRange: ClosedRange<Date>? = nil,
    displayedComponents: DatePickerComponents = [.date]
)

Style Variants

Style Token Default
DFCompactDatePickerStyle .compact Yes
DFGraphicalDatePickerStyle .graphical No
DFWheelDatePickerStyle .wheel No
DFGlassDatePickerStyle .glass No — requires iOS 26+

Examples

// Date only, compact
DFDatePicker("Date of birth", selection: $birthdate)

// Date and time
DFDatePicker(
    "Scheduled for",
    selection: $scheduledDate,
    displayedComponents: [.date, .hourAndMinute]
)

// With min/max range
DFDatePicker(
    "Expiry date",
    selection: $expiryDate,
    in: Date()...oneYearFromNow
)

// Graphical calendar
DFDatePicker("Pick a date", selection: $meetingDate)
    .dfDatePickerStyle(.graphical)

DFCheckbox

A standalone checkbox component — distinct from DFToggle's .checkbox style.

Initializer Signature

public init(isChecked: Binding<Bool>, label: String = "")

Examples

// Minimal — terms agreement
DFCheckbox(isChecked: $agreed, label: "I agree to the Terms of Service")

// Multi-select list
ForEach(interests, id: \.self) { interest in
    DFCheckbox(
        isChecked: Binding(
            get: { selected.contains(interest) },
            set: { isOn in
                if isOn { selected.insert(interest) }
                else { selected.remove(interest) }
            }
        ),
        label: interest
    )
}

// Disabled
DFCheckbox(isChecked: $granted, label: "Access camera")
    .disabled(isLocked)

Validation State

DFValidationState is shared by DFTextField and DFSecureField:

public enum DFValidationState: Sendable, Equatable {
    case none            // No validation feedback shown
    case error(String)   // Red border + inline error message
    case valid           // Green border; no additional message
}

Pattern: Derive from a computed property

private var emailState: DFValidationState {
    guard !email.isEmpty else { return .none }
    let valid = email.contains("@") && email.contains(".")
    return valid ? .valid : .error("Enter a valid email address")
}

DFTextField("Email", text: $email, validationState: emailState) {
    Image(systemName: "envelope")
}

Building a Complete Form

import SwiftUI
import DesignFoundation

struct UserProfileForm: View {
    @State private var displayName = ""
    @State private var email = ""
    @State private var password = ""
    @State private var isPublic = true
    @State private var emailNotifications = false
    @State private var timezone = "UTC"
    @State private var birthday = Date()
    @State private var contentMaturity = 0.5
    @State private var agreedToTerms = false
    @State private var agreedToMarketing = false

    private var emailState: DFValidationState {
        guard !email.isEmpty else { return .none }
        return email.contains("@") ? .valid : .error("Enter a valid email address")
    }

    private var passwordState: DFValidationState {
        guard !password.isEmpty else { return .none }
        return password.count >= 8 ? .valid : .error("Must be at least 8 characters")
    }

    private let timezones = ["UTC", "America/New_York", "America/Los_Angeles", "Europe/London"]

    var body: some View {
        ScrollView {
            VStack(alignment: .leading, spacing: 24) {
                DFTextField("Display name", text: $displayName, placeholder: "Your public name") {
                    Image(systemName: "person")
                }

                DFTextField(
                    "Email",
                    text: $email,
                    placeholder: "you@example.com",
                    validationState: emailState
                ) {
                    Image(systemName: "envelope")
                }

                DFSecureField(
                    "Password",
                    text: $password,
                    placeholder: "At least 8 characters",
                    validationState: passwordState
                )

                DFPicker("Timezone", selection: $timezone) {
                    ForEach(timezones, id: \.self) { Text($0).tag($0) }
                }

                DFDatePicker("Birthday", selection: $birthday, in: ...Date())
                    .dfDatePickerStyle(.compact)

                DFSlider("Content maturity", value: $contentMaturity, in: 0...1)
                    .dfSliderStyle(.labeled)

                DFToggle("Public profile", isOn: $isPublic)
                DFToggle("Email notifications", isOn: $emailNotifications)

                DFCheckbox(isChecked: $agreedToTerms, label: "I agree to the Terms of Service")
                DFCheckbox(isChecked: $agreedToMarketing, label: "Send me product updates")

                DFButton("Save profile") { saveProfile() }
                    .disabled(!agreedToTerms)
                    .padding(.top, 8)
            }
            .padding()
        }
        .dfTextFieldStyle(.filled)
        .dfSecureFieldStyle(.filled)
    }

    private func saveProfile() { /* persistence logic */ }
}

See also: Buttons · Theming · Forms-and-Validation · Lists-Tables-and-Data · Getting-Started

Clone this wiki locally