-
Notifications
You must be signed in to change notification settings - Fork 0
Controls
Daniel S edited this page Jul 2, 2026
·
1 revision
This page is a deep reference for DesignFoundation's five interactive control components: DFToggle, DFSlider, DFCheckbox, DFPicker, and DFDatePicker. For a quick overview of all input components, see Text-Fields-and-Inputs.
| Scenario | Use |
|---|---|
| Turn a single feature on or off (immediate effect) |
DFToggle (.switch style) |
| Select one item from 2–4 options visible simultaneously |
DFPicker (.segmented) |
| Select one item from a long hidden list |
DFPicker (.menu) |
| Binary agreement in a form (collect, then submit) | DFCheckbox |
| Select multiple items independently |
DFCheckbox multi-select pattern |
| Pick a continuous value along a range | DFSlider |
| Pick a discrete stepped value |
DFSlider with step:
|
| Choose a calendar date | DFDatePicker |
| Choose a date and time |
DFDatePicker with displayedComponents
|
Toggle vs Checkbox: Use DFToggle when the action takes effect immediately (enabling push notifications). Use DFCheckbox when the user confirms a choice before submitting a form.
DFToggle("Enable notifications", isOn: $enabled)
DFToggle("Auto-save", isOn: $autoSave).dfToggleStyle(.switch) // default
DFToggle("Remember device", isOn: $remember).dfToggleStyle(.checkbox)
DFToggle("Auto-save", isOn: $autoSave).dfToggleStyle(.glass) // iOS 26+
DFToggle("Advanced mode", isOn: $advanced).disabled(!isPro).switch |
.checkbox |
|
|---|---|---|
| Platform metaphor | On/Off switch | Selection mark |
| Best for | System settings, instant-effect toggles | Forms, agreements |
| Perceived timing | "This happens now" | "This is my answer" |
struct NotificationSettingsSection: View {
@State private var pushEnabled = true
@State private var emailDigest = false
@State private var smsAlerts = false
var body: some View {
VStack(alignment: .leading, spacing: 12) {
DFText("Notifications", style: .headline)
DFCard {
VStack(spacing: 0) {
DFListRow("Push notifications",
subtitle: "Receive alerts on this device") {
DFToggle("", isOn: $pushEnabled)
}
DFDivider()
DFListRow("Email digest",
subtitle: "Daily summary at 8 AM") {
DFToggle("", isOn: $emailDigest)
}
DFDivider()
DFListRow("SMS alerts") {
DFToggle("", isOn: $smsAlerts)
.disabled(!pushEnabled)
}
}
}
}
.padding()
}
}struct FeatureFlagsView: View {
@EnvironmentObject var flags: FeatureFlags
var body: some View {
DFCard(padding: 16) {
VStack(alignment: .leading, spacing: 12) {
DFText("Experimental Features", style: .headline)
DFText("These features are in beta and may change.", style: .caption)
ForEach(flags.all) { flag in
HStack {
VStack(alignment: .leading, spacing: 2) {
DFText(flag.displayName, style: .body)
DFText(flag.description, style: .caption)
}
Spacer()
DFToggle("", isOn: flags.binding(for: flag.id))
.disabled(flag.isForced)
}
if flag.id != flags.all.last?.id { DFDivider() }
}
}
}
}
}DFCard {
VStack(spacing: 0) {
DFListRow("Share analytics") {
DFToggle("", isOn: $analyticsEnabled)
}
DFDivider()
DFListRow("Crash reporting") {
DFToggle("", isOn: $crashReporting)
.disabled(!analyticsEnabled)
}
DFDivider()
DFListRow("Performance monitoring") {
DFToggle("", isOn: $perfMonitoring)
.disabled(!analyticsEnabled)
}
}
}struct RegistrationFooter: View {
@Binding var agreedToTerms: Bool
@Binding var subscribeMarketing: Bool
let onSubmit: () -> Void
var body: some View {
VStack(alignment: .leading, spacing: 12) {
DFToggle("I agree to the Terms of Service and Privacy Policy",
isOn: $agreedToTerms)
.dfToggleStyle(.checkbox)
DFToggle("Send me product updates and announcements",
isOn: $subscribeMarketing)
.dfToggleStyle(.checkbox)
DFButton("Create account") { onSubmit() }
.disabled(!agreedToTerms)
}
}
}DFSlider(value: $opacity) // 0...1, no label
DFSlider("Volume", value: $volume, in: 0...100)
DFSlider("Quality", value: $quality, in: 1...5, step: 1)
DFSlider("Font size", value: $size, in: 10...36, step: 1)
.dfSliderStyle(.labeled) // shows label + current value + min/max captionsstruct AudioControlPanel: View {
@State private var volume: Double = 0.7
@State private var micGain: Double = 0.5
var body: some View {
DFCard {
VStack(spacing: 20) {
DFText("Audio Controls", style: .headline)
DFSlider("Volume", value: $volume, in: 0...1)
DFSlider("Microphone gain", value: $micGain, in: 0...1)
.dfSliderStyle(.labeled)
}
.padding()
}
}
}struct AppearanceSettings: View {
@AppStorage("fontSize") private var fontSize: Double = 16
var body: some View {
VStack(alignment: .leading, spacing: 16) {
DFSlider("Font size", value: $fontSize, in: 10...36, step: 1)
.dfSliderStyle(.labeled)
DFCard {
DFText("The quick brown fox jumps over the lazy dog.")
.font(.system(size: fontSize))
.padding()
}
}
}
}struct ExportSettingsView: View {
@State private var quality: Double = 3
private let qualityNames = ["", "Draft", "Low", "Medium", "High", "Maximum"]
var body: some View {
DFCard {
VStack(spacing: 24) {
DFText("Export Quality", style: .headline)
VStack(alignment: .leading, spacing: 4) {
HStack {
DFText("Quality", style: .body)
Spacer()
DFBadge(text: qualityNames[Int(quality)])
}
DFSlider(value: $quality, in: 1...5, step: 1)
.accessibilityLabel("Export quality")
.accessibilityValue(qualityNames[Int(quality)])
}
}
.padding()
}
}
}struct PriceRangeFilter: View {
@State private var minPrice: Double = 0
@State private var maxPrice: Double = 500
let absoluteMin: Double = 0, absoluteMax: Double = 1000
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
DFText("Price range", style: .body)
Spacer()
DFText("$\(Int(minPrice)) – $\(Int(maxPrice))", style: .caption)
}
DFSlider("Minimum", value: Binding(
get: { minPrice },
set: { minPrice = min($0, maxPrice - 10) }
), in: absoluteMin...absoluteMax, step: 10)
.dfSliderStyle(.labeled)
DFSlider("Maximum", value: Binding(
get: { maxPrice },
set: { maxPrice = max($0, minPrice + 10) }
), in: absoluteMin...absoluteMax, step: 10)
.dfSliderStyle(.labeled)
}
}
}DFCheckbox(isChecked: $agreed, label: "I agree to the Terms of Service")
DFCheckbox(isChecked: $selected) // no label — add accessibilityLabel
DFCheckbox(isChecked: $locked, label: "Admin only").disabled(true)struct InterestPicker: View {
let interests = ["Design", "Engineering", "Marketing", "Product"]
@State private var selected: Set<String> = []
private func binding(for interest: String) -> Binding<Bool> {
Binding(
get: { selected.contains(interest) },
set: { isOn in
if isOn { selected.insert(interest) }
else { selected.remove(interest) }
}
)
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
ForEach(interests, id: \.self) { interest in
DFCheckbox(isChecked: binding(for: interest), label: interest)
}
}
}
}struct TermsStep: View {
@State private var agreedTerms = false
@State private var agreedPrivacy = false
@State private var agreedMarketing = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
DFText("Before you continue", style: .headline)
DFCard {
VStack(alignment: .leading, spacing: 14) {
DFCheckbox(isChecked: $agreedTerms,
label: "I accept the Terms of Service")
DFDivider()
DFCheckbox(isChecked: $agreedPrivacy,
label: "I acknowledge the Privacy Policy")
DFDivider()
DFCheckbox(isChecked: $agreedMarketing,
label: "Send me product news (optional)")
}
.padding()
}
DFButton("Continue") { proceed() }
.disabled(!agreedTerms || !agreedPrivacy)
}
}
func proceed() { }
}struct OnboardingInterestsView: View {
struct Interest: Identifiable {
let id: String; let icon: String; let label: String
}
let interests: [Interest] = [
.init(id: "design", icon: "paintbrush", label: "UI/UX Design"),
.init(id: "ios", icon: "iphone", label: "iOS Development"),
.init(id: "backend", icon: "server.rack", label: "Backend Engineering"),
.init(id: "data", icon: "chart.bar", label: "Data & Analytics"),
]
@State private var selected: Set<String> = []
private func binding(for id: String) -> Binding<Bool> {
Binding(
get: { selected.contains(id) },
set: { if $0 { selected.insert(id) } else { selected.remove(id) } }
)
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
DFText("What are you interested in?", style: .headline)
DFText("Choose all that apply.", style: .caption)
LazyVGrid(columns: [.init(.flexible()), .init(.flexible())], spacing: 10) {
ForEach(interests) { interest in
DFCard(padding: 12) {
HStack(spacing: 8) {
DFIcon(interest.icon)
DFText(interest.label, style: .body)
Spacer()
DFCheckbox(isChecked: binding(for: interest.id))
}
}
.onTapGesture {
binding(for: interest.id).wrappedValue.toggle()
}
}
}
}
}
}struct PermissionsEditor: View {
enum Permission: String, CaseIterable, Identifiable {
case read = "Read", write = "Write", delete = "Delete", admin = "Administer"
var id: Self { self }
}
@Binding var granted: Set<Permission>
let isEditable: Bool
var body: some View {
DFCard {
VStack(spacing: 0) {
ForEach(Permission.allCases) { perm in
HStack {
DFText(perm.rawValue, style: .body)
Spacer()
DFCheckbox(isChecked: Binding(
get: { granted.contains(perm) },
set: { isOn in
if isOn { granted.insert(perm) }
else { granted.remove(perm) }
}
))
.disabled(!isEditable)
}
.padding(.vertical, 10)
if perm != Permission.allCases.last { DFDivider() }
}
}
.padding(.horizontal)
}
}
}DFPicker("Role", selection: $role) {
ForEach(Role.allCases, id: \.self) { r in Text(r.rawValue).tag(r) }
}
.dfPickerStyle(.menu) // default — dropdown
.dfPickerStyle(.segmented) // horizontal segmented control
.dfPickerStyle(.wheel) // spinning drum (iOS/iPadOS)
.dfPickerStyle(.glass) // iOS 26+Make your enum CaseIterable, Hashable, and Sendable:
enum ExportFormat: String, CaseIterable, Hashable, Sendable, Identifiable {
case pdf = "PDF", docx = "Word (.docx)", markdown = "Markdown"
var id: Self { self }
}struct InviteUserForm: View {
@State private var email = ""
@State private var role: Role = .viewer
@State private var team = "Engineering"
let teams = ["Design", "Engineering", "Marketing", "Product"]
var body: some View {
DFCard {
VStack(spacing: 16) {
DFText("Invite team member", style: .headline)
DFTextField("Email address", text: $email, leadingIcon: "envelope")
DFPicker("Role", selection: $role) {
ForEach(Role.allCases, id: \.self) { r in Text(r.rawValue).tag(r) }
}
DFPicker("Team", selection: $team) {
ForEach(teams, id: \.self) { Text($0).tag($0) }
}
DFButton("Send invite") { sendInvite() }
}
.padding()
}
}
func sendInvite() { }
}enum SortField: String, CaseIterable, Hashable, Sendable {
case updated = "Updated", name = "Name", created = "Created"
}
DFPicker("Sort", selection: $sortField) {
ForEach(SortField.allCases, id: \.self) { f in Text(f.rawValue).tag(f) }
}
.dfPickerStyle(.segmented)struct CountryPickerRow: View {
@State private var selectedCountry = "United States"
@State private var showingPicker = false
let countries = Locale.isoRegionCodes
.compactMap { Locale.current.localizedString(forRegionCode: $0) }
.sorted()
var body: some View {
DFListRow("Country", subtitle: selectedCountry, accessory: .navigation)
.onTapGesture { showingPicker = true }
.overlay {
DFSheet(isPresented: $showingPicker) {
VStack(spacing: 0) {
DFText("Select Country", style: .headline).padding()
DFPicker("Country", selection: $selectedCountry) {
ForEach(countries, id: \.self) { Text($0).tag($0) }
}
.dfPickerStyle(.wheel)
DFButton("Done") { showingPicker = false }.padding()
}
}
}
}
}struct ThemePickerRow: View {
@Binding var appTheme: AppTheme
var body: some View {
VStack(alignment: .leading, spacing: 12) {
DFPicker("Theme", selection: $appTheme) {
ForEach(AppTheme.allCases, id: \.self) { theme in
HStack {
Circle().fill(Color(hex: theme.accentHex)).frame(width: 12, height: 12)
Text(theme.rawValue)
}
.tag(theme)
}
}
// Live preview swatch
DFCard {
HStack(spacing: 12) {
DFAvatar(name: "Alex Kim")
VStack(alignment: .leading) {
DFText("Alex Kim", style: .headline)
DFText("Product Designer", style: .caption)
}
Spacer()
DFButton("Follow") { }.dfButtonStyle(.secondary)
}
.padding()
}
.environment(\.dfTheme, appTheme.preset.theme)
}
}
}DFDatePicker("Birthday", selection: $birthdate) // date only
DFDatePicker("Meeting", selection: $date, displayedComponents: [.date, .hourAndMinute])
DFDatePicker("Reminder at", selection: $time, displayedComponents: .hourAndMinute)
// Date range clamping
DFDatePicker("Appointment", selection: $date, in: Date()...) // future only
DFDatePicker("Birthday", selection: $birthdate, in: ...Date()) // past only
DFDatePicker("Expiry", selection: $expiry, in: Date()...oneYearFromNow) // bounded
// Styles
.dfDatePickerStyle(.compact) // default — tappable label, overlay picker
.dfDatePickerStyle(.graphical) // full calendar grid inline
.dfDatePickerStyle(.wheel) // spinning drums (use in sheet/popover)
.dfDatePickerStyle(.glass) // iOS 26+struct ProfileBirthdateRow: View {
@State private var birthdate = Calendar.current.date(
from: DateComponents(year: 1990, month: 1, day: 1))!
private var maximumDate: Date {
Calendar.current.date(byAdding: .year, value: -13, to: Date())!
}
private var formattedAge: String {
let comps = Calendar.current.dateComponents([.year], from: birthdate, to: Date())
return comps.year.map { "\($0) years old" } ?? ""
}
var body: some View {
DFCard {
VStack(alignment: .leading, spacing: 12) {
DFText("Date of birth", style: .headline)
DFDatePicker("Birthday", selection: $birthdate, in: ...maximumDate)
.dfDatePickerStyle(.compact)
DFText(formattedAge, style: .caption)
}
.padding()
}
}
}struct EventSchedulerView: View {
@State private var startDate = Date()
@State private var endDate = Date().addingTimeInterval(3600)
private var duration: String {
let interval = endDate.timeIntervalSince(startDate)
let h = Int(interval) / 3600; let m = (Int(interval) % 3600) / 60
return h > 0 ? "\(h)h \(m)m" : "\(m) minutes"
}
var body: some View {
DFCard {
VStack(alignment: .leading, spacing: 16) {
DFText("Schedule Event", style: .headline)
DFDatePicker("Starts", selection: $startDate, in: Date()...,
displayedComponents: [.date, .hourAndMinute])
.dfDatePickerStyle(.compact)
.onChange(of: startDate) { _, newStart in
if endDate <= newStart {
endDate = newStart.addingTimeInterval(3600)
}
}
DFDatePicker("Ends", selection: $endDate, in: startDate...,
displayedComponents: [.date, .hourAndMinute])
.dfDatePickerStyle(.compact)
HStack {
DFIcon("clock")
DFText("Duration: \(duration)", style: .caption)
}
DFButton("Add to calendar", icon: "calendar.badge.plus") { addToCalendar() }
.dfButtonStyle(.secondary)
}
.padding()
}
}
func addToCalendar() { }
}struct SubscriptionExpiryEditor: View {
@State private var expiryDate: Date
private let maxDate = Calendar.current.date(byAdding: .year, value: 5, to: Date())!
init(currentExpiry: Date) { _expiryDate = State(initialValue: currentExpiry) }
private var isExpiringSoon: Bool {
expiryDate.timeIntervalSince(Date()) < 30 * 24 * 3600
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack {
DFText("Expiry Date", style: .headline)
Spacer()
if isExpiringSoon { DFBadge(text: "Expiring soon", color: .orange) }
}
DFCard {
DFDatePicker("Expiry", selection: $expiryDate, in: Date()...maxDate)
.dfDatePickerStyle(.graphical)
.padding(.vertical, 8)
}
DFText(
"Active until \(expiryDate.formatted(date: .long, time: .omitted))",
style: .caption
)
}
}
}import DesignFoundation
struct AccountSettingsForm: View {
@Environment(\.dfTheme) private var theme
@State private var displayName = "Alex Kim"
@State private var role: AccountRole = .manager
@State private var birthdate = Date()
@State private var language: Language = .english
@State private var fontSize: Double = 16
@State private var reducedMotion = false
@State private var pushEnabled = true
@State private var emailEnabled = false
@State private var notifyOnMention = true
@State private var notifyOnComment = true
@State private var agreedMarketing = false
@State private var isSaving = false
var body: some View {
ScrollView {
VStack(alignment: .leading, spacing: 24) {
// Profile
sectionHeader("Profile", icon: "person.crop.circle")
DFCard {
VStack(spacing: 16) {
DFTextField("Display name", text: $displayName, leadingIcon: "person")
DFPicker("Role", selection: $role) {
ForEach(AccountRole.allCases, id: \.self) { r in
Text(r.rawValue).tag(r)
}
}
DFDatePicker("Date of birth", selection: $birthdate, in: ...Date())
}
.padding()
}
// Preferences
sectionHeader("Preferences", icon: "slider.horizontal.3")
DFCard {
VStack(spacing: 16) {
DFPicker("Language", selection: $language) {
ForEach(Language.allCases) { lang in Text(lang.rawValue).tag(lang) }
}
DFDivider()
DFSlider("Text size", value: $fontSize, in: 12...24, step: 1)
.dfSliderStyle(.labeled)
DFDivider()
DFListRow("Reduce motion") {
DFToggle("", isOn: $reducedMotion)
}
}
.padding()
}
// Notifications
sectionHeader("Notifications", icon: "bell")
DFCard {
VStack(spacing: 0) {
VStack(spacing: 16) {
DFListRow("Push notifications") { DFToggle("", isOn: $pushEnabled) }
DFListRow("Email") { DFToggle("", isOn: $emailEnabled) }
}
DFDivider().padding(.vertical, 12)
VStack(alignment: .leading, spacing: 12) {
DFText("Notify me when", style: .caption)
DFCheckbox(isChecked: $notifyOnMention, label: "Someone mentions me")
DFCheckbox(isChecked: $notifyOnComment, label: "Someone comments")
}
}
.padding()
}
// Consent
DFCard {
DFCheckbox(isChecked: $agreedMarketing,
label: "Send me product updates and offers")
.padding()
}
DFButton("Save changes", isLoading: isSaving) { saveSettings() }
.padding(.top, 8)
}
.padding()
}
}
@ViewBuilder
private func sectionHeader(_ title: String, icon: String) -> some View {
HStack(spacing: 6) {
DFIcon(icon, size: .sm, color: theme.colors.primary)
DFText(title.uppercased(), style: .caption)
}
}
private func saveSettings() {
isSaving = true
Task {
try? await Task.sleep(for: .seconds(1))
isSaving = false
DFToastQueue.shared.show("Settings saved", style: .success)
}
}
}-
labelstring becomesaccessibilityLabel. Always provide one. - Inside
DFListRow, the row title doesn't reach the toggle — still set the label.
- Always provide
accessibilityLabelandaccessibilityValuefor stepped sliders.
DFSlider("Quality", value: $quality, in: 1...5, step: 1)
.accessibilityLabel("Export quality")
.accessibilityValue(qualityNames[Int(quality)])- The
labelis announced as label + role ("checkbox, unchecked"). - For checkboxes without visible labels, supply
accessibilityLabelexplicitly.
- The picker label announces as "Role, popup button, currently Viewer."
- Never leave the label empty.
- The
.graphicalstyle exposes individual days as focusable accessibility elements. - Use
accessibilityHintwhen range clamping may surprise the user.
DFDatePicker("Return date", selection: $returnDate, in: departureDate...)
.accessibilityHint("Must be on or after your departure date")See also: Text-Fields-and-Inputs · Buttons · Lists-Tables-and-Data · Alerts-and-Toasts · Theming