```html SwiftUI: How to Build a Settings Screen (iOS 17+, 2026)

How to Build a Settings Screen in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: Form, Section Updated: May 11, 2026
TL;DR

Wrap rows inside a Form and group them with Section views. SwiftUI automatically renders the familiar inset-grouped list style that matches the system Settings app.

struct SettingsView: View {
    @AppStorage("notifications") private var notifications = true
    @AppStorage("theme") private var theme = "System"

    var body: some View {
        NavigationStack {
            Form {
                Section("Appearance") {
                    Picker("Theme", selection: $theme) {
                        Text("System").tag("System")
                        Text("Light").tag("Light")
                        Text("Dark").tag("Dark")
                    }
                }
                Section("Notifications") {
                    Toggle("Enable Notifications", isOn: $notifications)
                }
            }
            .navigationTitle("Settings")
        }
    }
}

Full implementation

The example below builds a realistic settings screen with four sections: Appearance, Notifications, Account, and About. Each section demonstrates a different row type — Picker, Toggle, NavigationLink, and a plain label row — all bound to @AppStorage so preferences persist automatically across launches.

import SwiftUI

// MARK: - Settings Model

enum AppTheme: String, CaseIterable, Identifiable {
    case system = "System"
    case light  = "Light"
    case dark   = "Dark"
    var id: String { rawValue }
}

// MARK: - Sub-screen for Account

struct AccountSettingsView: View {
    @AppStorage("username") private var username = "Ada Lovelace"

    var body: some View {
        Form {
            Section("Profile") {
                TextField("Display Name", text: $username)
                    .autocorrectionDisabled()
            }
            Section {
                Button("Sign Out", role: .destructive) {
                    // handle sign-out
                }
            }
        }
        .navigationTitle("Account")
        .navigationBarTitleDisplayMode(.inline)
    }
}

// MARK: - Main Settings Screen

struct SettingsView: View {
    @AppStorage("appTheme")           private var appTheme       = AppTheme.system.rawValue
    @AppStorage("notificationsOn")    private var notificationsOn = true
    @AppStorage("soundEnabled")       private var soundEnabled    = true
    @AppStorage("badgeCount")         private var badgeCount      = true
    @AppStorage("analyticsEnabled")   private var analyticsEnabled = false

    private let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0"

    var body: some View {
        NavigationStack {
            Form {

                // ── Appearance ────────────────────────────────────
                Section {
                    Picker("Theme", selection: $appTheme) {
                        ForEach(AppTheme.allCases) { theme in
                            Text(theme.rawValue).tag(theme.rawValue)
                        }
                    }
                } header: {
                    Text("Appearance")
                }

                // ── Notifications ─────────────────────────────────
                Section {
                    Toggle("Enable Notifications", isOn: $notificationsOn)
                    Toggle("Sounds", isOn: $soundEnabled)
                        .disabled(!notificationsOn)
                    Toggle("Badge App Icon", isOn: $badgeCount)
                        .disabled(!notificationsOn)
                } header: {
                    Text("Notifications")
                } footer: {
                    Text("Badge and sound settings require notifications to be enabled.")
                        .font(.caption)
                }

                // ── Account ───────────────────────────────────────
                Section("Account") {
                    NavigationLink("Account Settings") {
                        AccountSettingsView()
                    }
                    Toggle("Share Analytics", isOn: $analyticsEnabled)
                }

                // ── About ─────────────────────────────────────────
                Section("About") {
                    LabeledContent("Version", value: appVersion)
                    Link("Privacy Policy",
                         destination: URL(string: "https://soarias.com/privacy")!)
                    Link("Terms of Service",
                         destination: URL(string: "https://soarias.com/terms")!)
                }

            }
            .navigationTitle("Settings")
        }
    }
}

// MARK: - Preview

#Preview {
    SettingsView()
}

How it works

  1. Form as the root container. Form automatically adopts the .insetGrouped list style on iOS — you get the rounded, card-like section appearance of Apple's Settings app with zero extra modifiers.
  2. Section with header and footer. Passing a string literal to Section("Notifications") creates a header. The trailing footer: closure renders a caption below the group, ideal for explaining why certain rows are disabled.
  3. @AppStorage for persistence. Every toggle and picker is bound directly to @AppStorage, which reads and writes UserDefaults under the hood. No viewModel boilerplate is required for simple scalar preferences.
  4. .disabled(!notificationsOn) for dependent rows. The Sounds and Badge rows use .disabled tied to the parent toggle. SwiftUI grays them out and ignores taps automatically — no extra if branching needed.
  5. NavigationLink for drill-down screens. Wrapping AccountSettingsView() in a NavigationLink inside a NavigationStack gives free push/pop navigation with the standard back button and title animation.

Variants

Add icons to rows with Label

Section("Notifications") {
    Toggle(isOn: $notificationsOn) {
        Label("Enable Notifications", systemImage: "bell.badge")
    }
    Toggle(isOn: $soundEnabled) {
        Label("Sounds", systemImage: "speaker.wave.2")
    }
    Toggle(isOn: $badgeCount) {
        Label("Badge App Icon", systemImage: "app.badge")
    }
}

// Tint each icon independently
Label("Theme", systemImage: "paintbrush")
    .foregroundStyle(.purple)

Present as a sheet instead of a pushed screen

If your app doesn't have a persistent NavigationStack, present SettingsView() modally. Wrap it in its own NavigationStack so the title and any sub-screens render correctly:

// In your parent view:
@State private var showSettings = false

Button("Settings") { showSettings = true }
    .sheet(isPresented: $showSettings) {
        NavigationStack {           // ← required inside sheet
            SettingsView()
                .toolbar {
                    ToolbarItem(placement: .confirmationAction) {
                        Button("Done") { showSettings = false }
                    }
                }
        }
    }

Common pitfalls

Prompt this with Claude Code

When using Soarias or Claude Code directly to implement this:

Implement a settings screen in SwiftUI for iOS 17+.
Use Form and Section for grouped layout.
Persist preferences with @AppStorage.
Include Toggle, Picker, NavigationLink, and LabeledContent rows.
Make it accessible (VoiceOver labels on every row).
Add a #Preview with realistic sample data.

In Soarias, drop this prompt into the Build phase after your screens are scaffolded — Claude Code will wire up @AppStorage keys to match the rest of your data model and insert the settings sheet into your existing navigation flow automatically.

Related

FAQ

Does this work on iOS 16?
Yes, almost entirely. Form, Section, Toggle, Picker, and @AppStorage all exist since iOS 14. The only iOS 17-specific feature used above is the #Preview macro — replace it with a PreviewProvider struct to drop back to iOS 16. LabeledContent requires iOS 16+ specifically. For iOS 15 targets, swap it for a plain HStack { Text(key); Spacer(); Text(value) }.
How do I share settings values across multiple views without passing bindings everywhere?
Two solid approaches. First, use @AppStorage with the same key in every view that needs it — SwiftUI keeps them in sync automatically since they all read the same UserDefaults slot. Second, create an @Observable class (Swift 5.9+) that exposes @AppStorage-backed properties, inject it with .environment(), and read it with @Environment. The observable approach is better when multiple preferences change together and you want atomic validation before saving.
What's the UIKit equivalent?
In UIKit you'd use a UITableViewController with style .insetGrouped, hand-coding sections as arrays and populating cells with UISwitch, UIStepper, or disclosure chevrons. You'd also manage didSelectRow callbacks and separate data sources manually. SwiftUI's Form/Section collapses all of that into declarative view composition, roughly cutting the boilerplate by 70–80%.

Last reviewed: 2026-05-11 by the Soarias team.

```