Popular Searches
Popular Course Categories
Popular Courses

Managing UI State with @State, @Binding, and Observable Objects

Managing UI State with @State, @Binding, and Observable Objects

Modern iOS UI Development with SwiftUI

Managing UI State with @State, @Binding, and Observable Objects in SwiftUI

Course: iOS App Development

Topic: Managing UI State with @State, @Binding, and Observable Objects

Course Link: iOS App Development Training at JustAcademy

Demo Link: Register for a Free Course Demo

Official Website: JustAcademy


1. Introduction to UI State Management

UI state management is the process of storing, updating, and sharing the data that determines what an application displays on the screen. In iOS development, an interface may change when a user taps a button, enters text, selects an option, opens a menu, or receives updated information from a service.

SwiftUI provides a declarative approach to building interfaces. Developers describe how the interface should appear for a particular state, and SwiftUI updates the relevant views when the data changes.

The three important concepts covered in this topic are @State, @Binding, and observable objects. They help developers organize data, share values between views, and keep application interfaces synchronized with changing information.

Learning Objectives

  • Understand UI state and reactive interface updates.
  • Learn how to declare and manage local state using @State.
  • Understand how @Binding shares editable state between views.
  • Learn how observable objects communicate changes to SwiftUI.
  • Understand @Observable, ObservableObject, @StateObject, and @ObservedObject.
  • Build reusable views that share application data.
  • Apply state management concepts in practical iOS applications.
  • Identify common state management mistakes and debugging techniques.

2. What Is UI State?

UI state represents the current condition of an application's interface. It includes information such as whether a button is selected, whether a dialog is visible, what text a user has entered, and which items appear in a list.

Examples of UI State

State Variable Data Type Purpose
isLoggedIn Bool Tracks whether a user is logged in.
counter Int Stores a numeric counter value.
username String Stores text entered by the user.
isDarkMode Bool Tracks a theme preference.
selectedTab Int Identifies the currently selected tab.
isShowingAlert Bool Controls whether an alert appears.
items Array Stores a collection displayed in a list.

How State Changes Affect the Interface

  1. The application stores a value.
  2. A SwiftUI view reads that value to determine its appearance.
  3. The user or application changes the value.
  4. SwiftUI detects the relevant state change.
  5. The affected parts of the interface are updated.

This approach reduces the need to manually refresh interface elements after every data change.

3. Understanding @State

@State is a SwiftUI property wrapper used to store a value that belongs to a view and its local interface behavior. SwiftUI manages the storage and updates dependent views when the value changes.

It is suitable for simple values such as integers, strings, Boolean flags, and arrays that a view needs to modify.

Key Characteristics of @State

  • Stores state owned by a SwiftUI view.
  • Supports mutable values.
  • Allows the interface to respond to changes automatically.
  • Is commonly declared with the private access modifier.
  • Can provide a binding to its value through the dollar-sign syntax.
  • Works best when the state belongs to the view and its subviews rather than a shared application-wide model.

Example: Counter Using @State

import SwiftUI

struct CounterView: View {
    @State private var count = 0

    var body: some View {
        VStack(spacing: 16) {
            Text("Count: \(count)")
                .font(.largeTitle)

            Button("Increase Count") {
                count += 1
            }

            Button("Reset Count") {
                count = 0
            }
        }
        .padding()
    }
}

Code Explanation

  • import SwiftUI imports the SwiftUI framework.
  • CounterView defines a view that displays a counter.
  • @State private var count = 0 creates a state value initialized to zero.
  • Text("Count: \(count)") displays the current counter value.
  • count += 1 increases the counter when the button is tapped.
  • count = 0 resets the counter.

Whenever the counter changes, SwiftUI recalculates the relevant view body and displays the new value.

4. Using @State with Text Fields

SwiftUI's TextField needs access to editable text. A state variable can store the text, while a binding connects the TextField to that variable.

Example: Username Input

import SwiftUI

struct UsernameView: View {
    @State private var username = ""

    var body: some View {
        VStack(spacing: 16) {
            TextField("Enter your username", text: $username)
                .textFieldStyle(.roundedBorder)

            Text("Hello, \(username.isEmpty ? "Guest" : username)!")
        }
        .padding()
    }
}

Explanation

  • username stores the entered text.
  • $username provides a Binding to the state value.
  • TextField reads and writes the username through the binding.
  • The greeting updates when the user enters or deletes text.

The dollar sign is important because TextField requires a binding for its editable text value rather than a plain String.

5. Understanding @Binding

@Binding creates a two-way connection to a value owned elsewhere. It allows a child view to read and modify data without owning the original state.

For example, a parent view might own a Boolean value that determines whether a setting is enabled. A reusable child view can receive a binding to that value and allow the user to change it.

Key Characteristics of @Binding

  • Does not create independent storage for the underlying value.
  • Connects a view to state owned by another component.
  • Allows changes made by a child view to update the original value.
  • Uses the dollar-sign syntax when passing a binding from a state property.
  • Helps create reusable and modular SwiftUI components.

Example: Parent and Child Views

import SwiftUI

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

    var body: some View {
        VStack(spacing: 16) {
            Text(isEnabled ? "Setting Enabled" : "Setting Disabled")

            SettingToggleView(isEnabled: $isEnabled)
        }
        .padding()
    }
}

struct SettingToggleView: View {
    @Binding var isEnabled: Bool

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

How It Works

  1. The parent view creates and owns the isEnabled state.
  2. The parent passes $isEnabled to the child view.
  3. The child receives the binding through @Binding.
  4. The Toggle reads the current value and allows the user to change it.
  5. The original state changes, and the parent's text updates automatically.

6. Difference Between @State and @Binding

Feature @State @Binding
Primary purpose Owns view-local state. Accesses state owned elsewhere.
Storage SwiftUI manages the state storage. Does not own the underlying value.
Typical location Parent or owning view. Child or reusable subview.
Declaration @State private var count = 0 @Binding var count: Int
Passing data Pass the value for read-only use or a binding for editing. Receives a binding from another source of truth.
Example Owns a selected option. Allows a child picker to change that option.

7. Understanding Observable Objects

When an application grows, state may need to be shared by several views. Instead of placing every property inside one view, developers can create a model object that manages related data and business logic.

In SwiftUI, observable models allow views to respond to changes in model properties. There are two important approaches:

  • Modern Observation: Use the @Observable macro, available on supported Apple platforms starting with iOS 17 and corresponding platform releases.
  • Combine-based observation: Use the ObservableObject protocol with @Published properties and the appropriate SwiftUI property wrappers.

These approaches are related but use different observation mechanisms. Choose the approach that fits your deployment target and project architecture.

8. Modern Observable Models Using @Observable

The @Observable macro makes a model's stored properties observable. SwiftUI can track the properties a view reads and update dependent views when those properties change.

This approach is available in iOS 17 and later, as well as corresponding releases of other Apple operating systems.

Example: User Profile Model

import SwiftUI
import Observation

@Observable
class UserProfile {
    var name = "Alex"
    var age = 25
    var isPremium = false
}

Explanation

  • @Observable adds observation support to the class.
  • name stores the user's name.
  • age stores the user's age.
  • isPremium represents a Boolean account setting.
  • Views that read these observable properties can update when the properties change.

9. Managing an Observable Model with @State

For modern Observation-based models, a SwiftUI view can store the model instance using @State. SwiftUI manages the model reference and tracks observable properties read by the view.

Example: Profile Management Screen

import SwiftUI
import Observation

@Observable
class UserProfile {
    var name = "Alex"
    var age = 25
    var isPremium = false
}

struct ProfileView: View {
    @State private var profile = UserProfile()

    var body: some View {
        VStack(spacing: 16) {
            Text("Name: \(profile.name)")
            Text("Age: \(profile.age)")

            Toggle("Premium Membership", isOn: $profile.isPremium)

            Button("Change Name") {
                profile.name = "Jordan"
            }
        }
        .padding()
    }
}

In this example, the view owns the observable model. The displayed name and age react to model changes, and the Toggle receives a binding to the observable model's Boolean property.

10. Sharing Observable Models Between Views

An observable model can be passed to multiple views so that each view can display or modify the data it needs. The model provides a shared source of truth without requiring every property to be copied into separate state variables.

Example: Parent and Child Views with @Observable

import SwiftUI
import Observation

@Observable
class TaskModel {
    var taskName = "Learn SwiftUI"
    var isCompleted = false
}

struct TaskParentView: View {
    @State private var task = TaskModel()

    var body: some View {
        VStack(spacing: 16) {
            Text("Task: \(task.taskName)")

            TaskStatusView(task: task)
        }
        .padding()
    }
}

struct TaskStatusView: View {
    var task: TaskModel

    var body: some View {
        VStack(spacing: 12) {
            Text(task.isCompleted ? "Completed" : "In Progress")

            Button(task.isCompleted ? "Mark Incomplete" : "Complete Task") {
                task.isCompleted.toggle()
            }
        }
    }
}

The parent creates and owns the model. The child receives the same reference, so its changes affect the shared model. Because the child reads task.isCompleted, SwiftUI can update the dependent interface when that property changes.

11. Using @Bindable with Observable Models

When a child view needs to provide editable controls for properties of an @Observable model, @Bindable can create bindings to those properties.

Example: Editing a User Profile

import SwiftUI
import Observation

@Observable
class UserSettings {
    var username = "Alex"
    var notificationsEnabled = true
}

struct SettingsEditorView: View {
    @Bindable var settings: UserSettings

    var body: some View {
        Form {
            TextField("Username", text: $settings.username)

            Toggle(
                "Enable Notifications",
                isOn: $settings.notificationsEnabled
            )
        }
    }
}

struct SettingsParentView: View {
    @State private var settings = UserSettings()

    var body: some View {
        SettingsEditorView(settings: settings)
    }
}

Important distinction: @Binding connects a view to an existing value or binding, while @Bindable creates bindings to mutable properties of an Observation-based model. The model itself remains owned by its source of truth.

12. Using ObservableObject with @Published

Before the modern Observation approach, SwiftUI commonly used the Combine-based ObservableObject protocol. This remains useful for applications that support earlier operating system versions or already use this architecture.

The @Published property wrapper announces changes to a property so that observing SwiftUI views can update when the model changes.

Example: ObservableObject Model

import SwiftUI
import Combine

class CounterModel: ObservableObject {
    @Published var count = 0

    func increment() {
        count += 1
    }

    func reset() {
        count = 0
    }
}

Explanation

  • ObservableObject defines the model's observable-object behavior.
  • @Published publishes changes to the counter property.
  • increment() increases the counter.
  • reset() resets the counter to zero.

13. Understanding @StateObject and @ObservedObject

When working with Combine-based observable objects, SwiftUI provides property wrappers for managing ownership and observing changes.

@StateObject

@StateObject creates and manages an observable object instance for the lifetime of the owning view's identity. Use it when a view is responsible for creating and owning the model.

@ObservedObject

@ObservedObject observes an observable object supplied to a view, commonly by a parent. It does not establish ownership in the same way as @StateObject.

Example: Parent and Child with ObservableObject

import SwiftUI
import Combine

class CounterModel: ObservableObject {
    @Published var count = 0
}

struct CounterParentView: View {
    @StateObject private var model = CounterModel()

    var body: some View {
        VStack(spacing: 16) {
            Text("Count: \(model.count)")

            CounterChildView(model: model)
        }
        .padding()
    }
}

struct CounterChildView: View {
    @ObservedObject var model: CounterModel

    var body: some View {
        Button("Increase") {
            model.count += 1
        }
    }
}

The parent creates and owns the model using @StateObject. The child observes the same model through @ObservedObject. When the published count changes, both views can update because they depend on that model value.

14. Comparing Modern Observation and ObservableObject

Feature @Observable Approach ObservableObject Approach
Observation mechanism Swift Observation framework. Combine-based object publishing.
Model declaration @Observable class Model class Model: ObservableObject
Property annotation Ordinary stored properties can be tracked. Typically uses @Published.
Owning view Often uses @State. Often uses @StateObject.
Receiving view Can receive the model as a normal property. Often uses @ObservedObject.
Editable property bindings Use @Bindable where appropriate. Can use bindings projected from @ObservedObject.
Platform considerations Available on iOS 17 and later. Suitable for older deployment targets that support SwiftUI and Combine.

15. Choosing the Correct State Management Tool

Select a state management tool based on who owns the data, how many views need it, and whether a child needs permission to modify it.

Requirement Recommended Tool Reason
Store a local counter or Boolean value. @State The value belongs to the view.
Allow a child view to edit a parent's value. @Binding The child receives a connection to existing state.
Share a modern observable model. @Observable with @State The owning view manages the model and dependent views track properties they read.
Edit properties on a modern observable model. @Bindable Provides bindings to model properties.
Create a Combine-based observable model. @StateObject The view owns the model instance.
Observe a Combine-based model provided by a parent. @ObservedObject The receiving view reacts to published changes.

16. Practical Project: Task Management Application

Project Objective: Build a small task management interface using SwiftUI state and observable data.

Project Features

  • Display a task title.
  • Allow users to mark a task as completed.
  • Update the interface when the task status changes.
  • Use a reusable child view for task controls.
  • Provide a button to reset the task status.

Swift Implementation

import SwiftUI
import Observation

@Observable
class Task {
    var title = "Complete SwiftUI Practice"
    var isCompleted = false

    func toggleCompletion() {
        isCompleted.toggle()
    }

    func reset() {
        isCompleted = false
    }
}

struct TaskView: View {
    @State private var task = Task()

    var body: some View {
        VStack(spacing: 20) {
            Text(task.title)
                .font(.title2)

            Text(task.isCompleted ? "Task Completed" : "Task Pending")
                .foregroundStyle(
                    task.isCompleted ? .green : .secondary
                )

            TaskControls(task: task)

            Button("Reset Task") {
                task.reset()
            }
        }
        .padding()
    }
}

struct TaskControls: View {
    var task: Task

    var body: some View {
        Button(
            task.isCompleted ? "Mark Pending" : "Mark Completed"
        ) {
            task.toggleCompletion()
        }
    }
}

Project Explanation

  1. The Task model stores the title and completion status.
  2. The @Observable macro enables observation of model properties.
  3. The parent view owns the model using @State.
  4. The child view receives the same model instance.
  5. The user can toggle the task status.
  6. The parent displays the current status based on the shared model.
  7. The Reset Task button sets the completion state to false.

This project demonstrates shared observable data without requiring a separate binding for every model property. The child modifies the shared model, and views update when they depend on changed properties.

17. Common Mistakes and Troubleshooting

Problem Possible Cause Solution
View does not update after a value changes. The value is not stored or observed correctly. Check the state declaration and how the view reads the value.
Cannot pass a String to a TextField binding. A plain String was passed instead of a Binding. Pass the projected state value, such as $username.
Child changes do not affect parent state. The child stores a separate local value. Use @Binding when the child needs to edit parent-owned state.
ObservableObject changes do not refresh the view. A property is not published or the object is not observed correctly. Check @Published and the appropriate observing wrapper.
Model instance is recreated unexpectedly. The model's ownership or view identity is incorrect. Review the owning view and use the appropriate state mechanism.
Compiler error involving @ObservedObject. A modern Observation model is being used with a Combine-based wrapper. Use the appropriate Observation APIs for the model type.
Changes do not persist after restarting the app. UI state is stored only in memory. Use suitable persistence such as SwiftData, files, or a backend service.
Two views show inconsistent values. Each view may own a separate copy of the data. Share a common source of truth.

Debugging Best Practices

  • Identify which view or model owns each value.
  • Avoid creating duplicate state for the same piece of information.
  • Use @Binding only when a view needs a binding to existing state.
  • Choose the correct observation system for the model type.
  • Keep business logic out of large view bodies where practical.
  • Use clear names for state variables and model properties.
  • Test changes from both parent and child views.
  • Remember that UI state in memory is not automatically permanent storage.

18. Best Practices for SwiftUI State Management

  • Maintain a single source of truth: Avoid storing multiple independent copies of the same value.
  • Keep local state local: Use @State for data that belongs to a view and its subviews.
  • Share editable values through bindings: Use @Binding when a child must modify state owned by another view.
  • Separate models from presentation: Use observable models to organize shared application data and logic.
  • Select the right observation approach: Use @Observable for modern Observation-based models or ObservableObject when appropriate.
  • Keep views reusable: Pass only the values or model references that a child needs.
  • Use appropriate persistence: Store data separately when it must survive view recreation or application restarts.
  • Test state transitions: Verify loading, success, failure, empty, and completed states.

19. Interview Questions and Answers

Q1. What is UI state in SwiftUI?

UI state is the data that determines the current appearance and behavior of an interface.

Q2. What is @State?

@State is a property wrapper that allows SwiftUI to manage a value owned by a view and update dependent interface content when the value changes.

Q3. What is @Binding?

@Binding provides a two-way connection to a value owned elsewhere, allowing a child view to read and modify that value.

Q4. What is the difference between @State and @Binding?

@State manages view-owned storage, whereas @Binding provides access to existing storage owned by another component.

Q5. What is an observable object?

An observable object is a model whose changes can be observed by SwiftUI views so that relevant interface content can update.

Q6. What is @Observable?

@Observable is a macro from the Observation framework that adds observation support to a model's properties.

Q7. What is ObservableObject?

ObservableObject is a Combine-based protocol used by objects that publish changes for observing views.

Q8. What is the purpose of @Published?

@Published publishes changes to a property in a Combine-based observable object.

Q9. What is the difference between @StateObject and @ObservedObject?

@StateObject manages an observable object owned by a view, while @ObservedObject observes an object supplied to a view.

Q10. When should @Bindable be used?

@Bindable is useful when a view needs bindings to mutable properties of an @Observable model, such as when connecting model properties to TextField or Toggle.

Q11. Why does SwiftUI automatically update views?

SwiftUI tracks dependencies between views and their data. When relevant state or observable properties change, SwiftUI updates affected portions of the interface.

Q12. Does @State automatically save data permanently?

No. @State manages view-related state in memory. Permanent storage requires an appropriate persistence mechanism.

Q13. Can multiple views share the same observable model?

Yes. Multiple views can receive the same model instance and react to the observable properties they read.

Q14. Which observation approach is available on iOS 17 and later?

The modern Observation framework, including the @Observable macro, is available on iOS 17 and later.

20. Quick Revision Checklist

  • @State stores state owned by a view.
  • @Binding connects a view to state owned elsewhere.
  • The dollar sign provides a binding from suitable state properties.
  • @Observable enables modern property observation.
  • @Bindable creates bindings to mutable properties of observable models.
  • ObservableObject supports the Combine-based observation approach.
  • @Published announces changes to properties of ObservableObject models.
  • @StateObject manages an owned ObservableObject instance.
  • @ObservedObject observes an ObservableObject supplied to a view.
  • A single source of truth reduces inconsistent application state.
  • State in memory is not automatically persisted across application restarts.

21. Conclusion

Managing UI state is a fundamental skill in SwiftUI development. @State is appropriate for values owned by a view, @Binding allows child views to modify existing state, and observable models help organize data shared across multiple parts of an application.

Developers can use the modern @Observable approach or the Combine-based ObservableObject approach according to their deployment requirements. Understanding ownership, data flow, and observation helps create responsive, reusable, and maintainable iOS applications.

Practice these concepts by developing counters, settings screens, profile editors, and task management applications. These projects provide a strong foundation for advanced SwiftUI development, including navigation, persistence, networking, and application architecture.


Learn More with JustAcademy

Explore iOS app development training and strengthen your practical knowledge of Swift, SwiftUI, and native application development.

Course Details: Explore the iOS App Development Training Course

Free Demo: Register for a Course Demo

Official Website: Visit the JustAcademy Homepage

Previous Next
whatsapp