swiftui · tutorial · components · state

Extracting a reusable SwiftUI component cleanly

Published February 6, 2026 · Reviewed February 18, 2026 · 3 min read · beginner

What we’re doing

We start with a single, concrete SwiftUI view and extract part of it into a reusable component.

The goal is not just reuse, but:

  • clean ownership
  • predictable data flow
  • readable code

This tutorial focuses on how to extract, not just that you can.


Why this works

This approach relies on SwiftUI views being cheap, disposable values:


The starting point

Imagine a simple settings row:

struct SettingsRow: View {
    @State private var isEnabled = false

    var body: some View {
        HStack {
            Text("Enable notifications")
            Spacer()
            Toggle("", isOn: $isEnabled)
        }
        .padding()
    }
}

This works — but the toggle is tightly coupled to the row.


The problem with naive extraction

A common first attempt looks like this:

struct ToggleView: View {
    @State var isOn: Bool

    var body: some View {
        Toggle("", isOn: $isOn)
    }
}

This compiles, but it breaks the mental model.

Why?

  • The child now owns state it should not own
  • Changes do not propagate back to the parent
  • Reuse becomes unpredictable

The rule of thumb

State belongs to the view that decides.
Reusable components receive bindings.

If a view:

  • decides → it owns @State
  • reflects → it receives @Binding

Step 1: Identify ownership

In our example:

  • The screen decides whether notifications are enabled
  • The row and toggle merely reflect that decision

So state must move up.


Step 2: Lift state to the parent

struct SettingsView: View {
    @State private var notificationsEnabled = false

    var body: some View {
        SettingsRow(
            title: "Enable notifications",
            isOn: $notificationsEnabled
        )
    }
}

Now the parent owns the truth.


Step 3: Convert the row to a reusable component

struct SettingsRow: View {
    let title: String
    @Binding var isOn: Bool

    var body: some View {
        HStack {
            Text(title)
            Spacer()
            Toggle("", isOn: $isOn)
        }
        .padding()
    }
}

Key changes:

  • @State@Binding
  • configuration via parameters
  • no hidden ownership

Why this works

  • There is one source of truth
  • The parent controls behavior
  • The component is reusable in any context
  • SwiftUI can reason about updates correctly

A quick mental checklist

Before extracting a component, ask:

  • Does this view decide something?
    • Yes → @State
  • Does it only display or forward something?
    • Yes → @Binding

If the answer is unclear, extract later.


Common mistakes

  • Using @State in reusable views
  • Passing values instead of bindings
  • Adding logic to components that should stay dumb

Key takeaways

  • Reusability is about boundaries, not size
  • Bindings preserve ownership
  • Clean extraction makes SwiftUI code scale
  • If data flow feels weird, state is probably in the wrong place

Clean components come from clean thinking.

Related