Andy Kolean
Posted on July 14, 2024
1. Introduction
State management is a crucial aspect of building responsive and efficient applications. In Swift, @Observable
and @ObservedObject
are powerful tools that simplify this process, especially when working with SwiftUI. Understanding how to effectively use these property wrappers can help you manage state more efficiently and create more maintainable code.
Importance of Understanding @Observable
and @ObservedObject
These property wrappers are fundamental for managing state in SwiftUI. They provide a way to automatically update views when data changes, reducing the need for manual state updates and making your code more reactive and responsive.
Simplification of State Management
Both @Observable
and @ObservedObject
reduce the boilerplate code typically associated with state management. They allow developers to focus more on the business logic and less on the intricacies of state synchronization. By leveraging these property wrappers, you can write cleaner, more declarative code that is easier to read and maintain.
Overview of What Will Be Covered
In this post, we will explore the basics of @Observable
and @ObservedObject
, including their definitions, purposes, and how to use them effectively. We will provide practical examples to illustrate their use and compare their functionalities to help you decide when to use each. Finally, we will conclude with a summary and suggest next steps for further learning.
2. The Present: @Observable
@Observable
is a property wrapper introduced in Swift 5.9's Observation framework to simplify state management. It allows objects to automatically notify views about state changes, reducing the need for boilerplate code and manual state updates.
Definition and Purpose
@Observable
is used to mark a class as observable, meaning any changes to its properties will be automatically published to any observing views. This is particularly useful in SwiftUI, where the UI needs to react to changes in underlying data models. The key advantage of @Observable
is that it allows you to write simpler, more declarative code.
How to Use @Observable
To use @Observable
, you mark your class with the @Observable
attribute and declare your properties as usual. Here's a basic example using a view model:
import SwiftUI
import Observation
@Observable final class UserSettingsViewModel {
var username: String = "Guest"
var isLoggedIn: Bool = false
}
With @Observable
, any changes to username
or isLoggedIn
will be automatically detected by any SwiftUI view observing this class.
Example Usage
Here’s how you can use the UserSettingsViewModel
class in a SwiftUI view:
struct ContentView: View {
@Bindable var viewModel = UserSettingsViewModel()
var body: some View {
VStack {
if viewModel.isLoggedIn {
Text("Welcome, \(viewModel.username)!")
} else {
Text("Please log in.")
}
Button(action: {
viewModel.isLoggedIn.toggle()
}) {
Text(viewModel.isLoggedIn ? "Log Out" : "Log In")
}
}
}
}
In this example, the ContentView
automatically updates when isLoggedIn
changes, thanks to the @Observable
property wrapper.
Observing State Changes with withObservationTracking
The withObservationTracking
function is a powerful tool in the Observation framework that allows you to observe state changes in a more controlled manner. It is particularly useful outside of SwiftUI contexts where automatic observation isn't available.
Non-Recursive Observation
import SwiftUI
import Combine
@Observable final class SettingsViewModel {
var name: String = "Guest"
}
struct SettingsView: View {
@Bindable var viewModel = SettingsViewModel()
func observeChanges() {
withObservationTracking {
_ = viewModel.name
} onChange: {
print(viewModel.name)
}
}
var body: some View {
VStack {
Text("Username: \(viewModel.name)")
Button(action: {
viewModel.name = ["Alice", "Bob", "Charlie", "David"].randomElement() ?? "Guest"
}) {
Text("Change Username")
}
}
.padding()
.onAppear {
observeChanges()
}
}
}
struct ContentView: View {
var body: some View {
SettingsView()
}
}
How It Works:
-
Function Call: The
observeChanges
function is called when the view appears. -
Observation Setup: The function sets up observation on
viewModel.name
. -
Change Detection: When
viewModel.name
changes, theonChange
closure is triggered, printing the new value to the console. - Non-Recursive: This function does not set up continuous observation beyond the initial change detection, meaning it only observes once per view appearance.
In this non-recursive example, changes to viewModel.name
are tracked and printed when the view appears and subsequently when the button is pressed to change the name.
Recursive Observation
import SwiftUI
import Combine
@Observable final class SettingsViewModel {
var name: String = "Guest"
}
struct SettingsView: View {
@Bindable var viewModel = SettingsViewModel()
func observeChanges() {
withObservationTracking {
_ = viewModel.name
} onChange: {
print(viewModel.name)
Task { @MainActor in
await observeChanges()
}
}
}
var body: some View {
VStack {
Text("Username: \(viewModel.name)")
Button(action: {
viewModel.name = ["Alice", "Bob", "Charlie", "David"].randomElement() ?? "Guest"
}) {
Text("Change Username")
}
}
.padding()
.onAppear {
observeChanges()
}
}
}
struct ContentView: View {
var body: some View {
SettingsView()
}
}
How It Works:
-
Function Call: The
observeChanges
function is called when the view appears. -
Observation Setup: The function sets up observation on
viewModel.name
. -
Change Detection: When
viewModel.name
changes, theonChange
closure is triggered, printing the new value to the console. -
Recursive Call: The
onChange
closure calls theobserveChanges
function recursively to ensure continuous observation.
Important Note
The onChange
trailing closure is invoked on the leading edge of the mutation, that is, when the mutation is about to happen but hasn’t yet actually happened. Additionally, because it is difficult to predict when the Task
closure will be executed, using @MainActor
within the Task
ensures that the updates occur on the main thread. This prevents the risk of reading an old value of the state.
In this recursive example, the observation restarts every time a change is detected, ensuring continuous monitoring of changes to viewModel.name
.
By following this structure, you can effectively use withObservationTracking
to observe state changes in a controlled manner. This approach ensures that your code remains efficient and responsive to state updates.
Use Cases:
withObservationTracking
is useful for scenarios where you need fine-grained control over state changes, such as logging, analytics, or complex state synchronization tasks outside of SwiftUI.
Using @Bindable
for SwiftUI Bindings
SwiftUI introduces the @Bindable
property wrapper to create bindings from the properties of any observable type. This wrapper simplifies creating and managing bindings within your SwiftUI views.
@Observable final class AuthViewModel {
var username = ""
var password = ""
var isAuthorized = false
func authorize() {
isAuthorized.toggle()
}
}
struct AuthView: View {
@Bindable var viewModel: AuthViewModel
var body: some View {
VStack {
if !viewModel.isAuthorized {
TextField("Username", text: $viewModel.username)
SecureField("Password", text: $viewModel.password)
Button("Authorize") {
viewModel.authorize()
}
} else {
Text("Hello, \(viewModel.username)")
}
}
}
}
In this example, the @Bindable
property wrapper is used to create bindings from the AuthViewModel
properties, allowing seamless state management within the SwiftUI view.
Challenges and Best Practices
While @Observable
greatly simplifies state management, it also introduces challenges, particularly with nested observable objects. Observing too much state can lead to inefficiencies, while observing too little can cause glitches. SwiftUI's new Observation framework addresses these challenges by ensuring views only observe the state they actually use, and automatically manage subscriptions.
For example, if a view conditionally observes state:
var isDisplayingSecondsElapsed = true
if self.model.isDisplayingSecondsElapsed {
Text("Seconds elapsed: \(self.model.secondsElapsed)")
}
Toggle(isOn: self.$model.isDisplayingSecondsElapsed) {
Text("Observe seconds elapsed")
}
Using @Bindable
:
struct ObservableCounterView: View {
@Bindable var model: CounterModel
// ...
}
The view stops observing secondsElapsed
when it’s no longer displayed, preventing unnecessary re-renders.
3. The Past: @ObservedObject
@ObservedObject
is a property wrapper used in SwiftUI to observe changes in an observable object. When the observed object changes, the view that uses it automatically updates. While @ObservedObject
was extensively used in earlier versions of SwiftUI, the new Observation framework simplifies some of its use cases. However, understanding @ObservedObject
is still essential for maintaining compatibility and dealing with legacy code.
Definition and Purpose
@ObservedObject
is used to mark a property as an observable object within a SwiftUI view. This means any changes to the properties of the observed object will trigger a re-render of the view. It is commonly used when the view does not own the lifecycle of the observed object but still needs to react to its changes.
How to Use @ObservedObject
To use @ObservedObject
, you typically define a class conforming to the ObservableObject
protocol, and then use @ObservedObject
to mark the property in your view.
import SwiftUI
import Combine
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
@Published var isLoggedIn: Bool = false
}
struct ContentView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
if settings.isLoggedIn {
Text("Welcome, \(settings.username)!")
} else {
Text("Please log in.")
}
Button(action: {
settings.isLoggedIn.toggle()
}) {
Text(settings.isLoggedIn ? "Log Out" : "Log In")
}
}
}
}
In this example, the ContentView
automatically updates when isLoggedIn
changes, thanks to the @ObservedObject
property wrapper.
Example Usage in a Real-World Scenario
Consider a more complex scenario where @ObservedObject
is used in a multi-view application.
class Task: ObservableObject {
@Published var title: String
@Published var isCompleted: Bool
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
struct TaskListView: View {
@ObservedObject var task: Task
var body: some View {
HStack {
Text(task.title)
Spacer()
Button(action: {
task.isCompleted.toggle()
}) {
Image(systemName: task.isCompleted ? "checkmark.square" : "square")
}
}
}
}
Here, the TaskListView
updates when the task's isCompleted
status changes.
Differences from @Observable
While @ObservedObject
and @Observable
both serve to notify views of changes, they have different use cases and benefits:
-
@Observable
simplifies the code by eliminating the need for manual@Published
properties andObservableObject
conformance. -
@Observable
integrates seamlessly with SwiftUI and other Swift data structures, making it more flexible and less verbose. -
@ObservedObject
requires explicit declaration of@Published
properties and conformance toObservableObject
.
When to Use Each
Use @Observable
for new code and when you want to take advantage of the streamlined syntax and automatic observation. Use @ObservedObject
for existing codebases that already use ObservableObject
and @Published
properties, or when you need finer control over the published properties.
Migration from @ObservedObject to @Observable
To migrate from @ObservedObject
to @Observable
, follow these steps:
- Remove
ObservableObject
conformance from your class. - Remove
@Published
property wrappers. - Add the
@Observable
macro to the class definition. - Replace
@ObservedObject
in your views with@Bindable
where necessary.
Example:
Before Migration:
class UserSettings: ObservableObject {
@Published var username: String = "Guest"
@Published var isLoggedIn: Bool = false
}
struct ContentView: View {
@ObservedObject var settings = UserSettings()
var body: some View {
VStack {
if settings.isLoggedIn {
Text("Welcome, \(settings.username)!")
} else {
Text("Please log in.")
}
Button(action: {
settings.isLoggedIn.toggle()
}) {
Text(settings.isLoggedIn ? "Log Out" : "Log In")
}
}
}
}
After Migration:
@Observable class UserSettings {
var username: String = "Guest"
var isLoggedIn: Bool = false
}
struct ContentView: View {
@Bindable var settings = UserSettings()
var body: some View {
VStack {
if settings.isLoggedIn {
Text("Welcome, \(settings.username)!")
} else {
Text("Please log in.")
}
Button(action: {
settings.isLoggedIn.toggle()
}) {
Text(settings.isLoggedIn ? "Log Out" : "Log In")
}
}
}
}
4. Comparison
In this section, we will compare @Observable
and @ObservedObject
to highlight their differences, use cases, and benefits. Understanding when to use each will help you make informed decisions in your SwiftUI projects.
Differences Between @Observable
and @ObservedObject
-
Declaration and Usage:
-
@Observable
: Simplifies the code by eliminating the need forObservableObject
conformance and@Published
properties. It uses the@Observable
macro to make a class observable. -
@ObservedObject
: Requires explicit conformance toObservableObject
and the use of@Published
for each observable property.
-
-
Property Wrappers:
-
@Observable
: Works seamlessly with the@Bindable
property wrapper, allowing easy binding creation within views. -
@ObservedObject
: Uses@Published
to mark observable properties and@ObservedObject
to mark the observing properties within views.
-
-
Ease of Use:
-
@Observable
: Offers a more declarative approach, reducing boilerplate and making the code easier to read and maintain. -
@ObservedObject
: Requires more boilerplate code, making it slightly more complex to manage.
-
-
Integration with SwiftUI:
-
@Observable
: Automatically integrates with SwiftUI, making it easier to use within SwiftUI views. -
@ObservedObject
: Works well with SwiftUI but requires manual setup of@Published
properties and conformance toObservableObject
.
-
-
Platform Availability:
-
@Observable
: Available only in iOS 17 and later, as well as the corresponding versions of macOS, tvOS, and watchOS. -
@ObservedObject
: Available in earlier versions of iOS and SwiftUI, providing broader compatibility for older devices and projects.
-
When to Use Each
-
Use
@Observable
:- For new codebases where you want to take advantage of the streamlined syntax and automatic observation.
- When you need a simpler, more declarative approach to state management.
- In scenarios where you want to avoid the manual boilerplate code associated with
@ObservedObject
and@Published
. - When targeting iOS 17 and later, and the corresponding versions of other Apple platforms.
-
Use
@ObservedObject
:- In existing codebases that already use
ObservableObject
and@Published
. - When you need finer control over which properties are published and observed.
- For compatibility with older versions of SwiftUI and iOS prior to Swift 5.9.
- In existing codebases that already use
Practical Example and Comparison
Consider the following practical example to illustrate the differences:
Using @ObservedObject
:
import SwiftUI
import Combine
class Task: ObservableObject {
@Published var title: String
@Published var isCompleted: Bool
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
struct TaskListView: View {
@ObservedObject var task: Task
var body: some View {
HStack {
Text(task.title)
Spacer()
Button(action: {
task.isCompleted.toggle()
}) {
Image(systemName: task.isCompleted ? "checkmark.square" : "square")
}
}
}
}
Using @Observable
:
import SwiftUI
import Observation
@Observable final class Task {
var title: String
var isCompleted: Bool
init(title: String, isCompleted: Bool = false) {
self.title = title
self.isCompleted = isCompleted
}
}
struct TaskListView: View {
@Bindable var task = Task(title: "Sample Task")
var body: some View {
HStack {
Text(task.title)
Spacer()
Button(action: {
task.isCompleted.toggle()
}) {
Image(systemName: task.isCompleted ? "checkmark.square
" : "square")
}
}
}
}
Performance Considerations
-
@Observable
: Efficiently tracks and observes only the properties used in the view, reducing unnecessary updates and improving performance. -
@ObservedObject
: Observes all@Published
properties, which can lead to inefficiencies if not managed properly.
Best Practices
-
@Observable
:- Use for new projects to leverage the latest advancements in the Observation framework.
- Take advantage of the automatic integration with SwiftUI and simplified syntax.
- Ensure proper handling of nested observable objects to avoid performance pitfalls.
- Be mindful of the iOS 17+ requirement and ensure your project targets the appropriate platform versions.
-
@ObservedObject
:- Use in existing projects or when migrating from older SwiftUI codebases.
- Maintain clear separation of concerns by using
@Published
properties judiciously. - Ensure compatibility with older SwiftUI and iOS versions when needed.
Conclusion
In this post, we explored the fundamentals of @Observable
and @ObservedObject
in Swift, understanding their purposes, usage, and differences. These property wrappers are essential tools for managing state in SwiftUI applications, each offering unique benefits and considerations.
Recap of Key Points
-
@Observable
: Introduced in Swift 5.9 and available in iOS 17+, this property wrapper simplifies state management by eliminating the need forObservableObject
conformance and@Published
properties. It integrates seamlessly with SwiftUI and reduces boilerplate code, making it ideal for new projects targeting the latest platforms. -
@ObservedObject
: A well-established property wrapper that requires explicit conformance toObservableObject
and the use of@Published
properties. It is suitable for existing projects and those needing compatibility with older SwiftUI and iOS versions.
Benefits of Adopting @Observable
-
Reduced Boilerplate:
@Observable
minimizes the need for repetitive code, making your SwiftUI views cleaner and more maintainable. -
Automatic Integration: With automatic observation handling,
@Observable
ensures your views update correctly without manual intervention. -
Enhanced Performance: By observing only the properties used in the view,
@Observable
reduces unnecessary updates, improving overall performance.
When to Use Each Tool
- Use
@Observable
for new projects, especially when targeting iOS 17+ and leveraging the latest Swift features. - Use
@ObservedObject
for existing projects, ensuring compatibility with older platforms and finer control over published properties.
References and Further Reading
- Apple Documentation on SwiftUI
- Swift Evolution Proposal for Observation Framework
- Point-Free Episode on SwiftUI and Observation
- WWDC 2023 Session on SwiftUI and Observable
Next Post
In the next post, we will delve deeper into the practical applications of @Perceptible
from the Perception library. We will explore how this annotation simplifies state management on older iOS versions with detailed examples and best practices for leveraging its full potential in SwiftUI applications.
Posted on July 14, 2024
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.