Hey, hi, hello! Today we're diving deep into the observer design pattern and how Swift and SwiftUI have brought a fresh take to this classic concept with Observables.
Observables are a new way to manage and respond to data changes in your app, providing more flexibility and type safety than ever before. Say goodbye to @Published
and @ObservableObject
and let's embrace Observables.
Observation supports subclassing of observable types. If you have an observable class that supports subclassing, don’t include the
final
keyword in the class declaration. Includingfinal
prevents subclassing of the type.
Note, you need to
import Observation
to use this functionality.
Creating an Observable Object
Creating an observable type is incredibly simple in Swift. All it requires is attaching the @Observable
macro to the type declaration. This macro takes care of everything needed to make the type observable, from declaring to implementing the Observable protocol. It does all of this during compile time, ensuring your runtime performance isn't impacted.
In our workout example, let's consider the Workout
class:
@Observable class Workout: Identifiable { var name: String = "Default Workout" var duration: Double = 30.0 var intensity: String = "Moderate" var inProgress: Bool = false }
In this example, our Workout
class is now observable, meaning any changes in its properties, such as name
, duration
, or intensity
, can be tracked and acted upon.
With our observable Workout
class in place, the Observation framework allows us to:
- Identify the
Workout
class as an observable type. - Keep an eye on alterations within any
Workout
instance. - Use these changes to trigger actions elsewhere, such as updating an app’s user interface.
The Observation framework handles the hard work of managing observers and notifications, allowing us to focus on reacting to the changes that matter in our application.
Bringing Observables into SwiftUI
SwiftUI's reactive nature works harmoniously with Swift's Observation framework. This potent combination allows for seamless tracking and reaction to changes in our app's state, directly influencing the UI. Let's see how this works in our workout scenario.
Consider we have a SwiftUI View
that displays our workout details. We can make this View
reactive to changes in an Observable
object.
struct WorkoutView: View { var workout: Workout var body: some View { VStack { Text("Workout Name: \(workout.name)") Text("Workout Duration: \(workout.duration) minutes") } } }
Here, WorkoutView
establishes a dependency on the workout
instance of our Observable
Workout
class. Whenever the workout
instance undergoes a change in its properties (name
, duration
, or intensity
), WorkoutView
automatically reflects these changes in the UI.
Note that here we have nothing in the view that observed the
intensity
property, so if any changes occur to the workout’s intensity, the view will not update.
In order to make changes to the view, we’ll add a Button
that toggles the workout state to “in progress”.
struct WorkoutView: View { var workout: Workout var body: some View { VStack { Text("Workout Name: \(workout.name)") Text("Workout Duration: \(workout.duration) minutes") Button { workout.inProgress.toggle() } label: { Text(workout.inProgress ? "End Workout" : "Start Workout") } } } }
Now that we’ve added a way to change the state of a property in out Observable
object, out view knows to track changes to this property and update the view accordingly, without needing to add the @Published
property wrapper.
With the power of Observation, we can track changes in computed properties that use another property in the class. Let's demonstrate this by adding a computed property status
to the Workout
class. The status
will be a string that tells us whether the workout is in progress or not and how long the workout will last.
Here's how we can update our Workout
class:
@Observable class Workout: Identifiable { var name: String = "Default Workout" var duration: Double = 30.0 var intensity: String = "Moderate" var inProgress: Bool = false var status: String { if inProgress { return "Workout in progress. Duration: \(duration) minutes." } else { return "Workout not started." } } }
The status
property is computed based on the inProgress
property and also incorporates the duration
property. When inProgress
is true
, it returns a string indicating the workout is in progress and shows the duration of the workout. If inProgress
is false
, it simply states that the workout has not started.
Now, let's incorporate this into our WorkoutView
:
struct WorkoutView: View { var workout: Workout var body: some View { VStack { Text("Workout Name: \(workout.name)") Text("Workout Duration: \(workout.duration) minutes") Text("Workout Intensity: \(workout.intensity)") Text(workout.status) Button { workout.inProgress.toggle() } label: { Text(workout.inProgress ? "End Workout" : "Start Workout") } } } /* Code to update workout duration */ }
We've added Text(workout.status)
to display the status of the workout. Also, in the button action, we've added an operation to increase the duration of the workout by 10 minutes each time the workout starts.
Now, when you toggle the workout's inProgress
state, SwiftUI will not only update the button label, but also the status
text. This is because the status
computed property depends on inProgress
and duration
, both of which SwiftUI is observing for changes.
Mastering Swift's Observable objects is crucial for SwiftUI development. They provide a powerful and flexible way to track state changes in your app and to keep your UI up-to-date with your data.
In this post, we've delved into creating an Observable object and using it within SwiftUI views. We've seen how changes to the Observable object's properties can be tracked and reflected in the UI. We've also explored how computed properties can be observed and how these changes affect our SwiftUI views.
The ability to correctly use and manage Observable objects in SwiftUI is an essential skill for any Swift developer. Keep experimenting and keep learning - I’d love to know how you use Observables in your projects.