Mastering SwiftUI State Management in 2025: @State, @Binding, @ObservedObject, @StateObject — Plus…
Confused between @State, @Binding, @ObservedObject, @StateObject, @Observable and @Bindable in SwiftUI? You’re not alone. This 2025 guide…

🚀 Introduction: Why State Management Is the Backbone of SwiftUI
If you’re building apps with SwiftUI and find yourself muttering things like “Why isn’t my view updating?” or “Why is this toggle broken again?!” — you’re not alone 😅
Welcome to the wild (but surprisingly elegant) world of SwiftUI state management.
Unlike UIKit, where you imperatively tell the UI what to do, SwiftUI flips the script. It says,
“Hey developer, just give me the data and I’ll figure out how the UI should look.”
This means your UI is a reflection of your data. And to make that work like magic, SwiftUI gives us tools like @State, @Binding, and @ObservedObject.
But here’s the twist:
Each of them looks similar but behaves very differently. Use the wrong one, and your UI might not update, or worse, you’ll be playing hide-and-seek with bugs you didn’t invite 🐛
In this guide, we’ll break down each of these three state tools with real-world examples, use cases, and a few gotchas along the way — so by the end, you’ll know what to use when, and why it matters. No PhD in reactive programming required 🧠✨
This 2025 guide includes both the traditional property wrappers (@State, @ObservedObject, etc.) and the modern Observation framework (@Observable, @Bindable) introduced in iOS 17 back in 2023. Whether you’re maintaining older apps or building fresh on iOS 17+, this guide has you covered
Ready? Let’s start with the humble @State.
🧠 Understanding State in SwiftUI
Before we dive into @State, @Binding, and the rest of the crew, let’s zoom out a bit.
In SwiftUI, state is the single source of truth that drives how your UI looks and behaves. If the state changes, the UI changes. If the state doesn’t change, SwiftUI just chills and leaves your pixels untouched 😎
Think of it like this:
🥤 Your app is a vending machine. 🧃 Your state is the drink inside. 🧑 The user presses a button (interacts with the view), and you update the drink (state). 🖥️ SwiftUI immediately changes the display to show the new drink.
No need to manually refresh the label or update constraints. SwiftUI watches your state like a hawk and re-renders the affected views when something changes.
But here’s the catch:
Not all state is created equal. Some state is local and should be kept private. Some needs to be shared with child views. And some needs to be managed in a view model.
That’s where our three main tools come in:
@State→ for simple, local value-type data@Binding→ to pass state down the view hierarchy@ObservedObject→ for class-based, sharable, more complex state
Next up, we’ll meet the simplest (but most common) of them all: @State.
🧱 @State: The Basics for Local View State
If SwiftUI were a band, @State would be the lead singer — always front and center, especially in beginner tutorials.
🧐 What is @State?
@State is a property wrapper that lets a view own and mutate a piece of data. When the data changes, SwiftUI automatically re-renders the view to reflect the change. Simple, clean, and elegant.
But there’s one rule:
@State is meant to be private. Only the view that declares it should modify it. It’s like a secret stash of UI logic tucked safely away in its own view.
🔧 When Should You Use It?
- You have a simple value type (like Bool, String, Int)
- The state is local to one view
- You don’t need to share or observe it from outside
✅ Code Example: Toggling a Dark Mode
struct DarkModeToggleView: View {
@State private var isDarkMode = false
var body: some View {
VStack {
Text(isDarkMode ? "🌙 Dark Mode" : "☀️ Light Mode")
.font(.largeTitle)
.padding()
Toggle("Enable Dark Mode", isOn: $isDarkMode)
.padding()
}
}
}Every time the user flips the toggle, the isDarkMode state changes, and SwiftUI says:
“Ah! The state changed. Time to update the view!”
Unlike UIKit, you don’t have to call reloadData() or setNeedsLayout() — SwiftUI handles the heavy lifting 🚀
⚠️ Gotcha: Don’t Share @State Around
You cannot pass a @State variable directly to another view. It’s meant to be private.
Want to let a child view read/write the same data? That’s where @Binding enters the scene (coming up next 👇).
🔗 @Binding: Passing State Between Views
Sometimes, your view has some state (managed using @State) that you want to share with a child view — but without giving up control. Think of @Binding as a two-way mirror 🪞: the child can see and change the value, but the actual data still lives in the parent.
🤝 What is @Binding?
@Binding lets a child view read and write a value that’s owned by its parent view. It doesn’t own the state — it’s just borrowing it with permission.
The key idea:
The parent uses_@State_, the child uses_@Binding_.
📦 When Should You Use It?
- You want to pass a modifiable value from one view to another
- The state lives in the parent, but the child needs to update it
- You’re working with value types, like Bool, String, etc.
✅ Code Example: Toggle Controlled by Parent
Let’s reuse our dark mode idea, but split it into parent-child:
struct ParentView: View {
@State private var isDarkMode = false
var body: some View {
ChildToggleView(isDarkMode: $isDarkMode)
}
}
struct ChildToggleView: View {
@Binding var isDarkMode: Bool
var body: some View {
Toggle("Enable Dark Mode", isOn: $isDarkMode)
.padding()
}
}Here’s what’s happening:
- ParentView owns the state via
@State - It passes a binding (
$isDarkMode) to the child ChildToggleViewreceives it using@Binding, so it can modify the same value
Boom — both views stay in sync without any extra work.
⚠️ Gotcha: Bindings Need a Source
You cannot initialize a **@Binding** variable on its own. SwiftUI will yell at you.
@Binding var someValue: Int = 0// ❌ You can't set this inside the viewIt must be passed in from another view that owns the state.
🧠 @ObservedObject: Managing Shared, Complex State
When your app grows beyond a few toggles and text fields, you’ll want to separate logic from views. That’s where @ObservedObject shines — it’s perfect for managing shared, class-based data across multiple views.
Think of it like your app’s state manager with superpowers.
🧪 What is @ObservedObject?
@ObservedObject is a property wrapper that lets your SwiftUI view observe a reference type (class) that conforms to ObservableObject. Any changes inside that object will trigger view updates, as long as the changed properties are marked with @Published.
In other words:
It’s how SwiftUI views listen to changes in an external class and update themselves accordingly 📻
🔍 When Should You Use It?
- You have shared state between multiple views
- Your data is class-based (reference type)
- You want fine-grained control over your data model or business logic
- You need to extract state into view models for testability and reuse
✅ Code Example: ViewModel with Counter
class CounterViewModel: ObservableObject {
@Published var count = 0
func increment() {
count += 1
}
}
struct CounterView: View {
@ObservedObject var viewModel: CounterViewModel
var body: some View {
VStack(spacing: 16) {
Text("Count: \(viewModel.count)")
.font(.title)
Button("Increase") {
viewModel.increment()
}
}
.padding()
}
}Usage in a parent view:
struct ContentView: View {
var body: some View {
CounterView(viewModel: CounterViewModel())
}
}- CounterViewModel owns the logic and state
@ObservedObjectlistens to changes via@Published- When count changes, SwiftUI auto-rebuilds the view — smooth as butter 🧈
⚠️ Gotcha: Forgetting @Published = Silent Fails
If you forget to add **@Published** to a property, changes to it won’t trigger UI updates. You’ll be tapping your button in frustration while SwiftUI stares blankly 😐
Also, note: recreating the object on every render (like CounterViewModel() inside .body) will reset your state. Always keep your view model outside the body, or use @StateObject (we’ll cover that in future chapters).
🎁 Bonus: Wait, What About @StateObject?
So far, we’ve been juggling @State, @Binding, and @ObservedObject. But hold up — there’s a fourth sibling peeking from the shadows: @StateObject.
It’s here to solve one of the most common bugs in SwiftUI apps.
🚨 The Problem It Solves
Remember how we said recreating a view model with @ObservedObject inside a view can reset its state every time the view redraws?
struct MyView: View {
var body: some View {
CounterView(viewModel: CounterViewModel()) // ⚠️ Bad! Resets every time
}
}That’s where @StateObject steps in like:
“Let me own and initialize the object. I’ll keep it alive across re-renders.” 🦸♂️
🔧 When to Use @StateObject
- You’re creating a new observable object inside a view
- You want that object to persist across view redraws
- Think of it as
@State, but for reference types
✅ Example: The Right Way to Own a ViewModel
class TimerViewModel: ObservableObject {
@Published var seconds = 0
}
struct TimerView: View {
@StateObject private var viewModel = TimerViewModel()
var body: some View {
Text("Seconds: \(viewModel.seconds)")
}
}No more resets. The TimerViewModel is created once, owned by the view, and survives redraws like a champ 💪
⚖️ @ObservedObject vs @StateObject
- Use
@ObservedObjectwhen the object is passed in - Use
@StateObjectwhen the object is created in the view
Mixing them up is the #1 way to create invisible bugs in SwiftUI — the kind that make you doubt reality 😵💫
🤯 What Happens If You Use @ObservedObject Instead of @StateObject
If you write this:
struct MyView: View {
@ObservedObject var viewModel = MyViewModel() // 🚨
var body: some View {
Text("Hello \(viewModel.name)")
}
}SwiftUI will compile it (no red flags), but here’s what goes wrong:
💣 The Bug (Without Crashing)
Every time SwiftUI re-renders MyView, it recreates the viewModel from scratch.
That means:
- Any state inside the view model is reset to its initial value
- Timers, API responses, text inputs, etc. get wiped
- You’ll see UI flickering, resetting, or just not updating properly
And worst of all? There are no error messages 😵💫 Just mysterious behavior.
🧠 Why It Happens
@ObservedObjecttells SwiftUI: “Hey, someone else owns this object. I’m just watching it.”- But if you’re creating the object inside the view, there is no external owner.
- SwiftUI doesn’t preserve it between view lifecycle updates — so it reinitializes it again and again.
✅ The Fix: Use @StateObject Instead
struct MyView: View {
@StateObject private var viewModel = MyViewModel()
var body: some View {
Text("Hello \(viewModel.name)")
}
}- Now SwiftUI owns the object and retains it.
- It creates it once, and preserves it even if the view redraws.
- No resets. No invisible bugs. Peace restored 🌈
📌 Summary: When to Use What
- ✅ Use
**@StateObject**When you are creating the observable object inside the view. SwiftUI will take ownership and make sure it doesn’t get recreated on every redraw. - ✅ Use
**@ObservedObject**When you are receiving the observable object from a parent view. The parent owns it — your view just observes changes.
🚀 @Observable and @Bindable (iOS 17+): The Future of State Management
Welcome to the cool kids’ club — say hello to @Observable and @Bindable, the dynamic duo that entered the SwiftUI scene with iOS 17 (Swift 5.9). They’re here to clean up your ObservableObject + @Published mess and make your state code feel ✨fresh and modern✨.
🧠 What is @Observable?
**@Observable** is a new macro-based property wrapper that marks your class or struct as observable by SwiftUI. It automatically tracks which properties change and updates the UI when needed — no more @Published, no more ObservableObject, and no more forgetting to make your types final.
🧪 Example:
@Observable
class UserSettings {
var username: String = ""
var isLoggedIn: Bool = false
}That’s it. No @Published, no ObservableObject, no fuss. Swift handles all the magic behind the scenes using the new Observation framework.
🔗 What is @Bindable?
If @Observable is the brains, @Bindable is the connector. It allows you to create bindings to specific properties inside your observable models — just like @Binding, but more powerful and precise.
You use it in a view when you want two-way binding with an @Observable object’s properties, especially useful in forms and input fields.
🧪 Example:
struct ProfileView: View {
@Bindable var user: UserSettings
var body: some View {
Form {
TextField("Username", text: $user.username)
Toggle("Logged In", isOn: $user.isLoggedIn)
}
}
}⚔️ How is This Different from the Old Way?
✅ What @Observable replaces:
- No more
ObservableObject - No more
@Publishedon every property - No need to make your model final
✅ What @Bindable replaces:
- Acts like
@Binding— but for properties inside@Observabletypes - No more awkward
$viewModel.propertyhacks - Two-way binding becomes clean and direct
🆚 Old vs New (The TL;DR)
Before (iOS 13–16):
- You had to use
ObservableObject+@Published - Views would observe the object using
@ObservedObjector@StateObject - To bind to a property, you needed something like
$object.property
Now (iOS 17+):
- Just mark your model with
@Observable - Use
@Bindablein your views for two-way updates - You can bind directly to nested properties (e.g.,
$user.name) without jumping through hoops
You might be wondering — isn’t_$user.name_something we already did with_@ObservedObject_? Technically yes, but the behavior is quite different. With the old system,_$user.name_relied on dynamic member lookup and_@Published_magic to dig into your_ObservableObject_. It looked like a direct binding, but it was really a clever workaround.
With_@Bindable_and the new Observation framework,_$user.name_is the real deal — a true binding to the name property inside your_@Observable_model. No more_@Published_, no hacks, and no runtime reflection. It’s faster, more precise, and feels exactly how SwiftUI bindings were always meant to work. 🔥
🚦 When Should You Use It?
If your app supports iOS 17+, there’s almost no reason not to use the new system. It’s cleaner, more efficient, and easier to reason about. But if you’re still supporting iOS 16 or earlier, you’ll have to stick with the classic approach.
📦 Bonus Tip: You Can Use @Observable in Structs Too!
Yup, unlike ObservableObject which only worked with classes, @Observable supports structs as well — making it a great fit for SwiftData models and lighter view models.
📝 This section exists thanks to a thoughtful reader’s (_Ken Turnbull_) comment:
“Your code uses the older ObservableObject and StateObject rather than the Observation Protocol which uses @Observable. My question was simply… how would all this code look if one was using @Observable and not ObservableObject?”
That’s a fantastic point — and something I didn’t initially include. So let’s clear that up.
Below is the updated version of the same logic, rewritten using the _modern @Observable + @Bindable approach_ introduced in iOS 17 (Swift 5.9). You’ll notice how much cleaner it is — no need for @Published, no need for ObservableObject, and no dynamic member lookup tricks. Just pure SwiftUI elegance. ✨
🧭 When to Use What (The No-Nonsense Guide)
By now, you’ve met the trio (and their overachieving cousin @StateObject) — but how do you decide which one to use and when?
Let’s simplify it with a set of “If this, then that” rules:
🟢 Use @State when:
- You have simple, local state
- You’re dealing with value types like
Bool,Int,String - You don’t need to share the state with other views
🧠 Think: “Private little secret that only this view needs to know.”
🔵 Use @Binding when:
- The parent owns the state using
@State - A child view needs to read and update that state
- You’re dealing with two-way data flow between views
🧠 Think: “Here, borrow my state — but give it back when you’re done.”
🟣 Use @ObservedObject when:
- You’re passing an observable object into a view
- The object is owned outside the view
- You want to observe changes without managing lifecycle
🧠 Think: “I’ll watch it, but someone else is in charge of keeping it alive.”
🟠 Use @StateObject when:
- You’re creating a new observable object inside the view
- You want SwiftUI to own and persist that object
- You don’t want it to be recreated on every render
🧠 Think: “This is my baby. I’ll raise it and make sure it stays alive.”
🧪 Use @Observable when:
✅ (iOS 17+ only)
- You want a cleaner, boilerplate-free observable model
- You don’t want to sprinkle
@Publishedeverywhere - You want to use the new SwiftUI Observation framework
🔗 Use @Bindable when:
✅ (iOS 17+ only)
- You need two-way binding to specific properties inside an
@Observablemodel - Especially useful for form fields, toggles, sliders, etc.
- Replaces awkward
@ObservedObject+$object.propertysetups
Still confused which one to use? If your app state is:
- Tiny & local → use
@State - Shared downward → use
@Binding - Shared across many → use
@ObservedObject - Created & owned locally → use
@StateObject - Clean observable model (iOS 17+) → use
@Observable - Bind to its properties (iOS 17+) → use
@Bindable
🧪 Real-World Mini Project: Toggle, Counter & Shared ViewModel
Let’s say you’re building a simple productivity app with:
- A toggle to enable “Focus Mode” (
@State) - A child view that controls that toggle (
@Binding) - A shared task counter (
@ObservedObject) - And the view model is created inside the view using (
@StateObject)
Yep — we’re using all four in one smooth, coordinated dance 💃🕺
🔨 Step 1: ViewModel with Shared State
class TaskViewModel: ObservableObject {
@Published var taskCount = 0
func addTask() {
taskCount += 1
}
func resetTasks() {
taskCount = 0
}
}🔧 Step 2: Child View with a Toggle Using @Binding
struct FocusToggleView: View {
@Binding var isFocusModeOn: Bool
var body: some View {
Toggle("Focus Mode", isOn: $isFocusModeOn)
.padding()
}
}🧩 Step 3: Child View with counter using @ObservedObject
struct TaskStatsView: View {
@ObservedObject var viewModel: TaskViewModel // @ObservedObject used here
var body: some View {
VStack {
Text("📊 Task Stats")
.font(.headline)
Text("You've completed \(viewModel.taskCount) tasks.")
.foregroundColor(.secondary)
}
}
}🧱 Step 4: Main View with All State Types
struct DashboardView: View {
@State private var isFocusModeOn = false // @State
@StateObject private var viewModel = TaskViewModel() // @StateObject
var body: some View {
VStack(spacing: 20) {
FocusToggleView(isFocusModeOn: $isFocusModeOn) // @Binding
Text("Tasks Completed: \(viewModel.taskCount)")
.font(.title)
Button("Add Task") {
viewModel.addTask()
}
Button("Reset") {
viewModel.resetTasks()
}
if isFocusModeOn {
Text("🔕 Notifications are silenced")
.foregroundColor(.gray)
}
Divider()
TaskStatsView(viewModel: viewModel)
}
.padding()
}
}🧠 Why This Works
@Statekeeps the toggle value local to the view@Bindingallows the child view to control the toggle@StateObjectowns the TaskViewModel instance safely inside the view@ObservedObjectis used within DashboardView behind the scenes to trigger UI updates when@Publishedproperties change
This kind of small-but-complete setup is something you’ll see often in real-world SwiftUI apps — and knowing which state tool to use where keeps your code clean, efficient, and bug-free.
🐛 Common Mistakes & Debugging Tips
SwiftUI’s state system is powerful… until you accidentally wire it wrong and your UI turns into a stubborn toddler — refusing to update, randomly resetting, or just plain ignoring you 😤
Here are the most common mistakes (and how to fix them):
❌ 1. Forgetting @Published in Your ObservableObject
“My _@ObservedObject_ isn’t triggering updates!”Why it happens:
You created a class conforming to ObservableObject, but forgot to mark its properties with @Published.
class MyViewModel: ObservableObject {
var count = 0 // ← No @Published = no UI update 😐
}Fix it:
@Published var count = 0 // ✅ View updates when this changes❌ 2. Creating the ViewModel in .body or as @ObservedObject
“Why does my state reset every time?”
If you write:
struct MyView: View {
@ObservedObject var viewModel = MyViewModel() // 😱
}SwiftUI will recreate this object every render, resetting your state. Silent, sneaky, and brutal.
Fix it:
Use @StateObject instead so the view owns and retains the instance:
@StateObject private var viewModel = MyViewModel() // ✅❌ 3. Using @State When You Should Use @ObservedObject
“Why can’t I access my view model’s properties?”
If you try something like this:
@State var viewModel = MyViewModel() // ❌ Doesn’t observe changesSwiftUI won’t observe changes inside your class because @State is for value types, not reference types.
Fix it: Use @StateObject or @ObservedObject depending on ownership.
❌ 4. Trying to Initialize a @Binding Without a Source
“Why is SwiftUI yelling at me about _@Binding_?”This fails:
@Binding var isEnabled: Bool
init() {
self._isEnabled = Binding.constant(true) // 😵💫 Usually not what you want
}Fix it: Always pass @Binding from a parent view that owns the original @State.
Or, for previews/testing, use .constant(true) intentionally:
#Preview {
FocusToggleView(isFocusModeOn: .constant(true)) // ✅
}🧠 Debugging Tip: Use print() or onChange
SwiftUI may be “magical,” but print(“State changed!”) still goes a long way when things don’t feel right.
You can also track changes reactively:
.onChange(of: viewModel.taskCount) { newValue in
print("Task count is now \(newValue)")
}And that’s it — you’ve officially leveled up your SwiftUI state skills ⚔️
🏁 Final Recap: What You Learned
SwiftUI’s state system can feel like a maze at first, but once you understand which tool to use and where — it becomes a superpower 💪
Here’s your cheat sheet for quick recall:
- ✅ @State — for simple, local value-type state
- 🔁 @Binding — for passing state down to child views
- 🧠 @ObservedObject — for observing shared, class-based state passed in
- 🧱 @StateObject — for owning and creating reference-type state inside a view
And remember:
Choose the right tool, and SwiftUI works like magic. Choose the wrong one… and SwiftUI gaslights you into thinking nothing is wrong 😅
You’ve now got the knowledge to build cleaner, smarter, and more predictable SwiftUI apps — and you can finally stop guessing which “@” to slap on your properties.
Next: Navigation in SwiftUI (2025 Edition)
Coming up in the next article of the SwiftUI Mastery Series, we’ll dive into:
- How to use NavigationStack in iOS 16+
- Handling deep linking and NavigationPath
- Building tab-based and nested navigation structures the right way in 2025
You’ll never get lost in a navigation flow again 🧭
**SwiftUI Navigation: NavigationStack, Deep Linking, and TabView Explained** _🧭 This guide demystifies SwiftUI’s navigation system — from TabView setups to deep linking and advanced…_medium.comhttps://medium.com/swift-pal/swiftui-navigation-navigationstack-deep-linking-and-tabview-explained-0f905bbb20d4
📺 Prefer video? This article is also available as a comprehensive video tutorial
🎉 Enjoyed this article? Your support means the world to me!
💬 Drop a comment below! I love hearing about your experiences and answering questions
🎬 Subscribe on Youtube and become early subscribers of my channel: https://www.youtube.com/@swift-pal
💼 Let’s connect on LinkedIn for more professional insights: https://www.linkedin.com/in/karan-pal
Happy coding! 🚀
New articles, straight to your inbox.
No spam, no filler — just new writing on iOS, the web, and AI when it ships. Unsubscribe anytime.
Keep reading
Why I'm Rebuilding My Blog From Scratch (and Leaving Medium)
After years of publishing on someone else's platform, I'm moving my writing to a home I actually own. Here's the reasoning, and what I'm building instead.
ReadData Is the Model: The Most Ignored Part of AI
A beginner-friendly guide to why data quality beats model hype.
Read