SwiftUI Tutorial

SwiftUI Toggle: Bindings, Styles, and a Custom ToggleStyle

A hands-on Toggle guide — bind it to state, swap between switch and button styles, build your own ToggleStyle, tint it to your brand, drop it inside Form and List, and gate premium features cleanly. Real Swift 6 code and pitfalls.

Last updated: 2026-07-17 8 min read By Ahmed Gagan, iOS Engineer
Quick Answer

A SwiftUI Toggle needs a Bool binding and a label: Toggle("Notifications", isOn: $enabled). Change its look with .toggleStyle(.switch), .toggleStyle(.button), or a custom type conforming to ToggleStyle. Color the switch with .tint(). Inside a Form or List each Toggle becomes a settings row automatically. To gate a premium feature, disable or intercept the toggle unless the user holds the required entitlement.

Required binding
A Binding<Bool> — usually $someState or a computed binding
Built-in styles
.switch (default on iOS) and .button
Customization
Conform to ToggleStyle for full control of the control
Swift Kit support
The Swift Kit's settings screen wires toggles to persisted @AppStorage and gates pro switches behind a RevenueCat entitlement check.

The binding is the whole point

A Toggle is a two-way control, so it always takes a Binding<Bool> plus a label. The simplest source is an @State property you pass with the $ prefix. Because it's a binding, the Toggle both reads the current value and writes the new one when the user flips it — you never manually respond to a tap. If you need to react to changes (persist a setting, fire analytics), attach .onChange(of:) to the binding's value rather than trying to intercept the toggle itself. For settings that must survive relaunches, back the state with @AppStorage so the value persists to UserDefaults automatically.

A persisted setting toggle
struct NotificationSetting: View {
    @AppStorage("pushEnabled") private var pushEnabled = true

    var body: some View {
        Toggle("Push Notifications", isOn: $pushEnabled)
            .onChange(of: pushEnabled) { _, newValue in
                NotificationManager.shared.setEnabled(newValue)
            }
    }
}

Built-in styles and tint

Toggle ships two built-in styles. The default on iOS is .switch, the familiar sliding capsule. .button turns the toggle into a bordered button that shows a selected state — handy in toolbars or filter bars where a switch would look out of place. Apply either with .toggleStyle. To recolor the switch's on state, use .tint(); the older .toggleStyle(SwitchToggleStyle(tint:)) initializer is deprecated, so prefer the standalone .tint modifier. Tint respects your accent color by default, which is why a single accent applied at the app root keeps every switch on-brand without per-toggle overrides.

  • .toggleStyle(.switch) — default sliding switch
  • .toggleStyle(.button) — bordered selectable button, great in toolbars
  • .tint(.green) — recolors the on state; replaces deprecated SwitchToggleStyle(tint:)
  • A label with an SF Symbol works well with .button style
Switch vs button styles with tint
struct StyleShowcase: View {
    @State private var wifi = true
    @State private var starred = false

    var body: some View {
        VStack(spacing: 24) {
            Toggle("Wi-Fi", isOn: $wifi)
                .toggleStyle(.switch)
                .tint(.green)

            Toggle(isOn: $starred) {
                Label("Star", systemImage: "star.fill")
            }
            .toggleStyle(.button)
        }
        .padding()
    }
}

Building a custom ToggleStyle

When neither built-in style fits — say you want a pill with a checkmark, or a card that highlights when on — conform a struct to ToggleStyle. Its makeBody(configuration:) receives a Configuration whose isOn Bool tells you the state and whose label carries your original label view. Crucially, you must toggle configuration.isOn.wrappedValue yourself in the tap handler, because your custom body replaces the default interaction. Wrap the flip in withAnimation for a smooth transition. Once written, apply it with .toggleStyle(MyStyle()) anywhere, and it composes with tint and other modifiers just like the built-ins.

A custom checkmark-pill ToggleStyle
struct CheckPillToggleStyle: ToggleStyle {
    func makeBody(configuration: Configuration) -> some View {
        HStack {
            configuration.label
            Spacer()
            Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle")
                .foregroundStyle(configuration.isOn ? .accentColor : .secondary)
        }
        .contentShape(Rectangle())
        .onTapGesture {
            withAnimation(.snappy) { configuration.isOn.toggle() }
        }
    }
}

// Usage:
// Toggle("Sync over cellular", isOn: $cellular)
//     .toggleStyle(CheckPillToggleStyle())

Toggles inside Form and List

Drop a Toggle into a Form or List and SwiftUI renders it as a native settings row — label on the left, control trailing-aligned, with correct insets and separators. This is how you build a Settings screen with almost no layout code. Group related switches under a Section with a header and footer to explain what each does. A subtle pitfall: if a toggle should enable or disable dependent rows, don't hide them abruptly — use .disabled(!parentToggle) so the rows stay visible but dimmed, which is less jarring and keeps the layout stable as the user flips the parent.

A settings Form built from toggles
struct SettingsForm: View {
    @AppStorage("notifications") private var notifications = true
    @AppStorage("sound") private var sound = true

    var body: some View {
        Form {
            Section("Alerts") {
                Toggle("Notifications", isOn: $notifications)
                Toggle("Play Sound", isOn: $sound)
                    .disabled(!notifications)
            }
        }
    }
}

Gating a premium feature with a toggle

A common product need is a toggle that only paying users may flip — enable AI suggestions, unlock cloud sync. The clean pattern is a computed Binding whose setter checks entitlement before writing, and whose getter returns the stored value. If the user lacks access, the setter presents your paywall instead of mutating state, so the switch springs back and the upsell appears. This keeps the gating logic in one place rather than sprinkling if isPro checks through the view. It also means the toggle visually reflects reality: off until purchased, on the moment the entitlement is granted.

  • Use a computed Binding to intercept writes before they land
  • In the setter, present a paywall when the entitlement is missing
  • Reflect the real entitlement in the getter so state stays honest
  • Prefer this over scattering isPro checks across the view body
A paywall-gated premium toggle
struct PremiumToggle: View {
    @State private var aiEnabled = false
    @State private var showPaywall = false
    let isPro: Bool

    var body: some View {
        let gated = Binding(
            get: { aiEnabled },
            set: { newValue in
                if isPro { aiEnabled = newValue }
                else { showPaywall = true }
            }
        )
        Toggle("AI Suggestions", isOn: gated)
            .sheet(isPresented: $showPaywall) { PaywallView() }
    }
}

Ship your SwiftUI app in 5 emails

A free 5-part course on the parts that actually stall launches: paywall, auth, onboarding, App Store review, and pricing. No fluff, unsubscribe anytime.

A settings screen you don't have to wire up

The Swift Kit's settings module ships toggles backed by @AppStorage and pro switches gated behind a RevenueCat entitlement — persistence and paywall interception already handled.

Get The Swift Kit — $99

Rather have it done for you? I set up and ship your app from $499. See how

How to build a custom ToggleStyle

A custom style lets you replace the switch entirely while keeping Toggle's binding semantics.

  1. 1

    Conform to ToggleStyle

    Create a struct that implements makeBody(configuration:).

    struct CardToggleStyle: ToggleStyle { func makeBody(configuration: Configuration) -> some View { /* ... */ } }
  2. 2

    Read the state

    configuration.isOn is a Binding<Bool>; configuration.label is your original label.

    let on = configuration.isOn.wrappedValue
  3. 3

    Flip it on tap

    Your body must mutate configuration.isOn itself, ideally inside withAnimation.

    .onTapGesture { withAnimation { configuration.isOn.toggle() } }
  4. 4

    Apply the style

    Attach it to any Toggle; it composes with tint and other modifiers.

    Toggle("Dark Mode", isOn: $dark).toggleStyle(CardToggleStyle())

Frequently Asked Questions

Why does my custom SwiftUI ToggleStyle not respond to taps even though makeBody renders correctly?
A custom ToggleStyle replaces the default interaction, so you are responsible for flipping the value. Add a tap gesture that calls configuration.isOn.toggle() (wrap it in withAnimation for smoothness). Without it the control looks right but never changes state.
How do I change the on-state color of a SwiftUI switch Toggle without using the deprecated SwitchToggleStyle(tint:)?
Use the standalone .tint(_:) modifier on the Toggle, for example Toggle("Wi-Fi", isOn: $on).tint(.green). The old SwitchToggleStyle(tint:) initializer is deprecated; .tint is the current, composable way to recolor the switch.
Can a SwiftUI Toggle drive a value that isn't a simple @State Bool, like an entitlement-gated setting?
Yes. Build a computed Binding<Bool> whose getter returns your stored value and whose setter runs logic — for example checking a RevenueCat entitlement and presenting a paywall instead of writing when the user isn't subscribed. Pass that binding to the Toggle.
Why do my dependent SwiftUI Form rows jump around when a parent Toggle is switched off?
Hiding rows with a conditional removes them from the layout and causes a jarring reflow. Instead keep the rows in the hierarchy and apply .disabled(!parentToggle) so they dim in place. The layout stays stable and the relationship reads clearly.
What's the difference between .toggleStyle(.switch) and .toggleStyle(.button) in SwiftUI?
.switch renders the familiar sliding capsule and is the iOS default, ideal for settings rows. .button renders a bordered button that shows a selected state, which suits toolbars and filter bars where a switch would look out of place. Both keep the same Bool binding.

Keep exploring

Ship settings and paywalls the first day

The Swift Kit is a $99 one-time SwiftUI boilerplate with a design system, RevenueCat paywalls, and a settings screen where toggles persist and gate premium features out of the box.

Get The Swift Kit — $99

One-time purchase · Lifetime updates · 14-day refund