How to implement a toggle switch in SwiftUI
Use SwiftUI's built-in Toggle view with a @State bool binding — it renders the native iOS switch control and wires up state automatically. Apply .toggleStyle() to customize the appearance.
import SwiftUI
struct ContentView: View {
@State private var isEnabled = false
var body: some View {
Toggle("Enable notifications", isOn: $isEnabled)
.padding()
}
}
Full implementation
The example below shows a settings-style form with multiple toggles, each backed by its own @State variable. Each Toggle is placed inside a Form with grouped sections, which gives you the standard iOS Settings look for free. A computed property drives a dependent UI element to demonstrate real-world conditional logic based on toggle state.
import SwiftUI
struct SettingsView: View {
@State private var notificationsEnabled = true
@State private var soundEnabled = true
@State private var darkModeEnabled = false
@State private var analyticsEnabled = false
// Derived: sound only matters when notifications are on
private var isSoundToggleDisabled: Bool {
!notificationsEnabled
}
var body: some View {
NavigationStack {
Form {
Section("Notifications") {
Toggle(isOn: $notificationsEnabled) {
Label("Enable notifications", systemImage: "bell.fill")
}
Toggle(isOn: $soundEnabled) {
Label("Sound", systemImage: "speaker.wave.2.fill")
}
.disabled(isSoundToggleDisabled)
.foregroundStyle(isSoundToggleDisabled ? .secondary : .primary)
}
Section("Appearance") {
Toggle(isOn: $darkModeEnabled) {
Label("Dark mode", systemImage: "moon.fill")
}
.tint(.indigo)
}
Section("Privacy") {
Toggle(isOn: $analyticsEnabled) {
Label("Share analytics", systemImage: "chart.bar.fill")
}
.tint(.green)
if analyticsEnabled {
Text("Anonymous usage data helps improve the app.")
.font(.footnote)
.foregroundStyle(.secondary)
}
}
}
.navigationTitle("Settings")
.navigationBarTitleDisplayMode(.large)
}
}
}
#Preview {
SettingsView()
}
How it works
-
@State bool binding — Each
@State private varowns the source of truth for one toggle. Prefixing with$passes a two-wayBinding<Bool>toToggle(isOn:), so the view re-renders automatically when the user flips the switch. -
Label view as the toggle's content — Using
Label("text", systemImage:)inside the trailing closure gives you an SF Symbol icon alongside the title, which VoiceOver reads as a single combined label automatically. -
.disabled() for dependent state — The
isSoundToggleDisabledcomputed property derives its value fromnotificationsEnabled. When the parent toggle is off,.disabled(true)greys out the child toggle and prevents interaction — no manual gesture blocking needed. -
.tint() for custom switch colors — Applying
.tint(.indigo)or.tint(.green)changes the "on" track color of the switch. This works on iOS 15+ and is the recommended replacement for the deprecatedaccentColor. -
Conditional child content — The
if analyticsEnabled { ... }block is a zero-animation way to show or hide explanatory text based on toggle state, keeping the form readable without extra view complexity.
Variants
Button-style toggle (checkbox look)
import SwiftUI
struct CheckboxToggleView: View {
@State private var isAccepted = false
var body: some View {
VStack(alignment: .leading, spacing: 16) {
// Built-in button style — renders as a tappable button, not a switch
Toggle(isOn: $isAccepted) {
Text("I accept the terms and conditions")
.font(.body)
}
.toggleStyle(.button)
.tint(isAccepted ? .green : .gray)
// Custom checkbox using a completely bespoke ToggleStyle
Toggle(isOn: $isAccepted) {
Text("Use custom checkbox style")
}
.toggleStyle(CheckboxToggleStyle())
}
.padding()
}
}
struct CheckboxToggleStyle: ToggleStyle {
func makeBody(configuration: Configuration) -> some View {
Button {
configuration.isOn.toggle()
} label: {
HStack {
Image(systemName: configuration.isOn
? "checkmark.square.fill"
: "square")
.foregroundStyle(configuration.isOn ? .blue : .secondary)
configuration.label
}
}
.buttonStyle(.plain)
}
}
#Preview {
CheckboxToggleView()
}
Observed object / @AppStorage toggle
For toggles that must persist across app launches, swap @State for @AppStorage:
struct PersistentToggleView: View {
// Reads and writes UserDefaults automatically
@AppStorage("notificationsEnabled") private var notificationsEnabled = true
var body: some View {
Toggle("Enable notifications", isOn: $notificationsEnabled)
.padding()
}
}
Common pitfalls
-
iOS 17+ only for onChange(of:initial:) — If you react to toggle changes with
.onChange(of: isEnabled), use the two-parameter closure form{ oldValue, newValue in }introduced in iOS 17. The single-parameter form was deprecated and produces a compiler warning in Swift 5.10. -
Forgetting to propagate .disabled() down the hierarchy — Applying
.disabled()to aSectiondisables every control inside it, which is convenient but easy to misuse — double-check that you are not accidentally disabling toggles you meant to keep active. -
Accessibility label duplication — When you put a
Labelview inside aToggle, VoiceOver automatically announces both the label text and "switch, on/off." Do not add a redundant.accessibilityLabel()unless you are replacing the label entirely, as it will cause double-speaking.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement a toggle switch in SwiftUI for iOS 17+. Use Toggle, @State (or @AppStorage for persistence), and .toggleStyle(). Make it accessible (VoiceOver labels, Label views with systemImage). Add a #Preview with realistic sample data showing multiple toggles in a Form.
Drop this prompt into the Soarias Build phase after your screens are mocked up — Claude Code will scaffold the toggle logic, wire bindings, and add accessibility annotations in one pass.
Related
FAQ
Does this work on iOS 16?
The core Toggle view and .toggleStyle(.button) work from iOS 15+. However, the two-parameter .onChange(of:initial:) closure and some Label rendering refinements are iOS 17+. If you target iOS 16, replace the new onChange with the deprecated single-parameter form and add @available guards where needed.
How do I change the toggle's "on" track color globally?
Apply .tint(Color) on the Toggle itself for per-control tinting, or on a parent container (like the whole Form) to change all toggles at once. Avoid the older .accentColor() modifier — it was deprecated in iOS 15 and has no effect on toggles in iOS 17+.
What's the UIKit equivalent?
In UIKit, UISwitch is the direct counterpart. You set its state via isOn and listen for changes using a .valueChanged target-action. SwiftUI's Toggle removes the boilerplate entirely — the binding replaces both the property write and the action handler.
Last reviewed: 2026-05-11 by the Soarias team.