Introduction
State management is one of the most important concepts in SwiftUI development. Unlike traditional UIKit applications where developers manually update views, SwiftUI uses a declarative approach where the interface automatically changes when data changes.
Understanding how data flows through SwiftUI is essential for building reliable applications. Incorrect state management can lead to unexpected UI updates, duplicated data, memory issues, and difficult-to-maintain code.
SwiftUI provides multiple property wrappers for managing different types of state. The correct choice depends on ownership, lifecycle, sharing requirements, and application architecture.

How SwiftUI State Works
SwiftUI follows a declarative UI model. Instead of telling the framework how to update each component, developers describe what the UI should look like for a specific state.
When state changes, SwiftUI recalculates the affected views and updates only the required parts of the interface.
struct CounterView: View {
@State
private var count = 0
var body: some View {
VStack {
Text(
"Count: \(count)"
)
Button("Increase") {
count += 1
}
}
}
}
What Is @State?
@State is the simplest SwiftUI property wrapper for storing local view state. SwiftUI manages the storage automatically and recreates the view whenever the value changes.
@State should be used when a view owns its own private data and no other component needs direct access to it.
- ●
Local view state
- ●
Boolean values
- ●
Text fields
- ●
Selections
- ●
Animations
- ●
Small temporary values
@State Example
struct SettingsView:
View {
@State
private var enabled =
false
var body: some View {
Toggle(
"Notifications",
isOn:
$enabled
)
}
}
Understanding View Identity
SwiftUI views are value types and can be recreated frequently. The @State property wrapper allows SwiftUI to preserve specific values between these recreations.
Without @State, changing a normal property inside a View would not trigger UI updates.
What Is @Binding?
@Binding allows one view to access and modify state owned by another view.
It creates a two-way connection between a parent view and a child view without transferring ownership of the data.
struct ParentView:
View {
@State
private var username = ""
var body:
some View {
UsernameField(
username:
$username
)
}
}
struct UsernameField:
View {
@Binding
var username:
String
var body:
some View {
TextField(
"Name",
text:
$username
)
}
}
@State vs @Binding
| @State | @Binding |
|---|---|
| Owns data | References existing data |
| Creates storage | Shares storage |
| Used by parent | Used by child |
| Private state | Two-way communication |
ObservableObject and @Published
For larger applications, state usually belongs inside dedicated objects called ViewModels. SwiftUI uses ObservableObject to observe these objects.
@Published marks properties that should notify SwiftUI when their values change.
class UserViewModel:
ObservableObject {
@Published
var username = ""
@Published
var isLoading =
false
}
@StateObject
@StateObject is used when a SwiftUI view creates and owns an ObservableObject instance.
SwiftUI keeps the object alive during view updates and manages its lifecycle.
struct ProfileView:
View {
@StateObject
private var viewModel =
ProfileViewModel()
var body:
some View {
Text(
viewModel.name
)
}
}
@ObservedObject
@ObservedObject is used when a view receives an existing ObservableObject from another part of the application. The view observes changes but does not own the object lifecycle.
Unlike @StateObject, SwiftUI does not create or preserve the object. The ownership belongs to another component.
struct ProfileScreen:
View {
@ObservedObject
var viewModel:
ProfileViewModel
var body:
some View {
VStack {
Text(
viewModel.name
)
}
}
}
@StateObject vs @ObservedObject
| @StateObject | @ObservedObject |
|---|---|
| Creates the object | Receives the object |
| Owns lifecycle | Does not own lifecycle |
| Used by parent/root views | Used by child views |
| Initializes ViewModel | Observes ViewModel |
💡 Rule of Thumb
If the View creates the ObservableObject, use @StateObject. If the View receives it from somewhere else, use @ObservedObject.
@EnvironmentObject
@EnvironmentObject allows shared objects to be injected into the SwiftUI environment and accessed by many views without manually passing them through every screen.
It is useful for global application state such as authentication, settings, themes, or user preferences.
class AppSettings:
ObservableObject {
@Published
var darkMode =
false
}
@main
struct MyApp:
App {
@StateObject
private var settings =
AppSettings()
var body:
some Scene {
WindowGroup {
ContentView()
.environmentObject(
settings
)
}
}
}
Using EnvironmentObject in Views
struct SettingsView:
View {
@EnvironmentObject
var settings:
AppSettings
var body:
some View {
Toggle(
"Dark Mode",
isOn:
$settings.darkMode
)
}
}
The Modern Observation Framework (@Observable)
Starting with iOS 17, Apple introduced the Observation framework as a modern replacement for many ObservableObject patterns.
The @Observable macro removes the need for @Published and provides automatic observation tracking.
import Observation
@Observable
class UserStore {
var username =
""
var isLoggedIn =
false
}
@Observable vs ObservableObject
| ObservableObject | @Observable |
|---|---|
| Combine based | Observation framework |
| @Published required | Automatic tracking |
| Older SwiftUI pattern | Modern iOS 17+ approach |
| Uses objectWillChange | More efficient tracking |
Using @Observable with SwiftUI
struct ContentView:
View {
@State
private var store =
UserStore()
var body:
some View {
Text(
store.username
)
}
}
Understanding State Ownership
The most important SwiftUI state management concept is understanding who owns the data. Every piece of state should have a clear owner.
| Situation | Recommended Property |
|---|---|
| Small local value | @State |
| Child modifies parent value | @Binding |
| View creates ViewModel | @StateObject |
| View receives ViewModel | @ObservedObject |
| Global shared state | @EnvironmentObject |
| iOS 17+ models | @Observable |
MVVM State Management in SwiftUI
The MVVM pattern is one of the most common architectures used with SwiftUI. The View displays state while the ViewModel manages business logic.
class ProductsViewModel:
ObservableObject {
@Published
var products:
[Product] = []
func loadProducts() {
// API call
}
}
struct ProductsView:
View {
@StateObject
var viewModel =
ProductsViewModel()
var body:
some View {
List(
viewModel.products
) {
product in
Text(
product.name
)
}
}
}
State Management with Combine
Combine is deeply connected with SwiftUI state management. ObservableObject and @Published use Combine publishers internally to notify views about changes.
@Published
var searchText = ""
$query
.debounce(
for:
.milliseconds(500),
scheduler:
DispatchQueue.main
)
.sink {
value in
search(value)
}
Form State Management
SwiftUI forms are a common place where state management becomes important. Text fields, toggles, validation messages, and submit actions all depend on state.
struct LoginView:
View {
@State
private var email =
""
@State
private var password =
""
var body:
some View {
Form {
TextField(
"Email",
text:
$email
)
SecureField(
"Password",
text:
$password
)
}
}
}
Navigation State Management
Modern SwiftUI navigation also depends on state. NavigationStack allows developers to control navigation using data-driven paths.
@State
private var path =
NavigationPath()
NavigationStack(
path:
$path
) {
HomeView()
}
State Restoration in SwiftUI
Applications often need to remember user progress after termination or when the system recreates views. SwiftUI provides several tools for preserving state.
| Property Wrapper | Purpose |
|---|---|
| @State | Temporary view state |
| @SceneStorage | Restore scene-specific values |
| @AppStorage | Persist UserDefaults values |
@AppStorage for Persistent State
@AppStorage connects SwiftUI state directly with UserDefaults. It is useful for simple settings and preferences.
struct SettingsView:
View {
@AppStorage(
"darkMode"
)
private var darkMode =
false
var body:
some View {
Toggle(
"Dark Mode",
isOn:
$darkMode
)
}
}
@SceneStorage for Scene State
@SceneStorage stores state related to a specific scene. It is commonly used for restoring navigation or user interface state.
struct SearchView:
View {
@SceneStorage(
"searchText"
)
private var searchText =
""
var body:
some View {
TextField(
"Search",
text:
$searchText
)
}
}
Dependency Injection and State Management
Large SwiftUI applications should avoid creating dependencies directly inside views. Dependency injection improves testing and keeps state management clean.
protocol UserService {
func users()
async throws
-> [User]
}
class UserViewModel:
ObservableObject {
private let service:
UserService
init(
service:
UserService
) {
self.service =
service
}
}
State Management with Services
A common production pattern is keeping application-wide state inside dedicated services and exposing only required data through ViewModels.
@Observable
class SessionManager {
var currentUser:
User?
var isAuthenticated:
Bool {
currentUser != nil
}
}
SwiftUI State Management Architecture

User Action
↓
SwiftUI View
↓
ViewModel
↓
Use Case / Service
↓
Repository
↓
Data Source
Common SwiftUI State Management Mistakes
- ●
Using @State for complex application data
- ●
Creating ViewModels inside body
- ●
Using multiple sources of truth
- ●
Passing too much data through views
- ●
Using EnvironmentObject everywhere
- ●
Ignoring ownership rules
- ●
Mixing UI and business logic
Creating Multiple Sources of Truth
One of the biggest SwiftUI architecture problems is storing the same information in multiple places.
When two different objects control the same value, they can easily become inconsistent and create unpredictable UI behavior.
💡 Best Practice
Each important piece of application data should have a single owner responsible for updating it.
SwiftUI State Performance Optimization
Good state management improves application performance because SwiftUI can efficiently determine which views need to be updated.
- ●
Keep state as local as possible
- ●
Avoid unnecessary object updates
- ●
Split large views into smaller components
- ●
Use Equatable views when needed
- ●
Avoid storing derived values
Derived State
Derived state is calculated from existing state instead of being stored separately.
struct CartView:
View {
let products:
[Product]
var total:
Double {
products
.reduce(0) {
result,
product in
result +
product.price
}
}
}
Testing SwiftUI State
State-driven architecture makes SwiftUI applications easier to test because behavior can be verified by changing state and checking output.
- ●
Test initial state
- ●
Test loading state
- ●
Test success state
- ●
Test error handling
- ●
Test user actions
Modern SwiftUI State Management Recommendation
| Scenario | Recommended Solution |
|---|---|
| Local UI value | @State |
| Child modification | @Binding |
| View-owned model | @StateObject |
| External model | @ObservedObject |
| Shared application data | @EnvironmentObject |
| New iOS 17+ apps | @Observable |
Frequently Asked Questions
Should I use ObservableObject or @Observable?
What is the difference between @State and @StateObject?
When should I use EnvironmentObject?
Is Combine required for SwiftUI?
Should ViewModels contain all application state?
Conclusion
SwiftUI state management is the foundation of modern iOS development. Understanding ownership, data flow, and property wrappers such as @State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject, and @Observable allows developers to build scalable applications with predictable behavior. Combined with MVVM, Combine, async/await, and dependency injection, SwiftUI provides a powerful architecture for professional iOS applications.
