SwiftUI - Tutorial 1 - ๐Ÿ”ฅ Understanding Data Flow in SwiftUI: @StateObject, @ObservedObject, and @EnvironmentObject


๐Ÿ”ฅ Understanding Data Flow in SwiftUI: @StateObject, @ObservedObject, and @EnvironmentObject

When you first start learning SwiftUI, all these property wrappers can feel confusing:

  • @State

  • @Binding

  • @StateObject

  • @ObservedObject

  • @EnvironmentObject

Don’t worry ๐Ÿ™Œ This guide will explain the three most important ones for managing data models in SwiftUI:

  • @StateObject

  • @ObservedObject

  • @EnvironmentObject

And we’ll also touch on the other related wrappers (@State, @Binding, @AppStorage, etc.).



๐ŸŸข 1. @StateObject – When the View Creates the Object

Use @StateObject when your view is responsible for creating and owning the object.
SwiftUI will keep this object alive as long as the view exists.

✅ Example:

class CounterViewModel: ObservableObject { @Published var count = 0 func increment() { count += 1 } } struct CounterView: View { @StateObject private var vm = CounterViewModel() // View owns it var body: some View { VStack { Text("Count: \(vm.count)") Button("Increment") { vm.increment() } } } }

๐Ÿ‘‰ Think of @StateObject as:

“This view creates the object and should keep it alive.”


๐ŸŸก 2. @ObservedObject – When the Object Comes From Outside

Use @ObservedObject when the parent view passes the object into this view.
The child view doesn’t own it, it just listens for changes.

✅ Example:

struct ChildView: View { @ObservedObject var vm: CounterViewModel // injected from parent var body: some View { Text("Child Count: \(vm.count)") } } struct ParentView: View { @StateObject private var vm = CounterViewModel() var body: some View { ChildView(vm: vm) // pass it down } }

๐Ÿ‘‰ Think of @ObservedObject as:

“I don’t own this object, but I care when it changes.”


๐Ÿ”ต 3. @EnvironmentObject – When Many Views Need the Same Object

Sometimes you don’t want to manually pass objects down through every view. That’s where @EnvironmentObject comes in.

You inject the object once at a higher level, and all child views can access it without explicitly passing it.

✅ Example:

struct ChildView: View { @EnvironmentObject var vm: CounterViewModel // no need to pass var body: some View { VStack { Text("Shared Count: \(vm.count)") Button("Increment") { vm.increment() } } } } @main struct MyApp: App { @StateObject private var vm = CounterViewModel() var body: some Scene { WindowGroup { ChildView() .environmentObject(vm) // inject once } } }

๐Ÿ‘‰ Think of @EnvironmentObject as:

“Put this object in the environment so anyone can use it.”


๐Ÿ”‘ Quick Comparison

WrapperWho Creates It?Scope of UseBest For
@StateObjectThe view itselfLocal viewView owns the object
@ObservedObjectParent viewPassed downChild observes parent’s object
@EnvironmentObjectInjected globallyEntire app (or subtree)Shared data across many views

๐Ÿ›  Other Property Wrappers You’ll See

  • @State → For simple values (Int, String, Bool).

    @State private var isOn = false
  • @Binding → Lets a child view read & write a parent’s state.

    struct ToggleView: View { @Binding var isOn: Bool var body: some View { Toggle("Enable", isOn: $isOn) } }
  • @AppStorage → Stores a value in UserDefaults and updates UI.

    @AppStorage("username") var username: String = ""
  • @SceneStorage → Stores view state per scene (useful for restoring state when app reopens).

  • @Environment → Access system values (color scheme, locale, etc.).

    @Environment(\.colorScheme) var colorScheme

๐ŸŽฏ Final Takeaway for Beginners

  • Use @StateObject when your view creates the ViewModel.

  • Use @ObservedObject when your view receives the ViewModel from outside.

  • Use @EnvironmentObject when you want to share the same object across many views.

With this mental model, you’ll know exactly which wrapper to reach for as your SwiftUI projects grow.



                          ┌─────────────────────┐

                          │     @AppStorage     │  → persisted in UserDefaults

                          │     @SceneStorage   │  → persisted per scene

                          └─────────────────────┘

                                     │

                                     ▼

                          ┌─────────────────────┐

                          │       @State        │  → local value state (Bool, Int, String…)

                          └─────────────────────┘

                                     │

                    ┌────────────────┴────────────────┐

                    ▼                                 ▼

        ┌─────────────────────┐             ┌─────────────────────┐

        │     @StateObject    │             │     @Environment    │ → built-in values

        │  (View creates VM)  │             │  (e.g. colorScheme) │

        └─────────────────────┘             └─────────────────────┘

                    │

                    ▼

        ┌─────────────────────┐

        │   ObservableObject  │

        │ (ViewModel / Model) │

        └─────────────────────┘

                    │

        ┌───────────┴───────────┐

        ▼                       ▼

┌───────────────────┐   ┌───────────────────┐

│  @ObservedObject  │   │ @EnvironmentObject │

│(child observes VM)│   │ (global VM shared) │

└───────────────────┘   └───────────────────┘


Comments

Popular posts from this blog

Your build is currently configured to use incompatible Java 21.0.3 and Gradle 8.2.1. Cannot sync the project.

Error in Android Migration Gradle 7.5 to 8.5 - java.lang.NullPointerException: Cannot invoke "String.length()" because "" is null

How to clean up the unused imports, variable and function from Typescript