Data flow is a crucial aspect of app development in SwiftUI, but it can often be challenging to manage both in terms of complexity and performance. In this video, we explore how observations can help simplify data flow and improve app performance in SwiftUI. The new framework for data persistence, SwiftData, is also using Observation.
⬇️ download the project from GitHub https://github.com/gahntpo/Swi…
Understanding Data Flow and Observations
Data flow in Swift UI involves various elements, such as property wrappers, observable objects, environments, state, and bindings. Observations provide an alternative approach to data flow, reducing the number of elements involved and making it easier to manage. By using observations, we can focus on two main property wrappers:
- @State
- @Bindable
Additionally, the new @Observable macro helps ensure proper data flow.
Simplifying Data Flow with Observations
Observations offer several benefits for data flow in SwiftUI. They streamline the code by eliminating the need for different types of view models and models. Instead, everything becomes a model, making the code more uniform and easier to work with. The @Observable macro simplifies the process by automatically handling data flow. This new approach makes it easier to write and manage model files in SwiftUI.
Improving App Performance with Observations
As apps become more complex with multiple views and shared state, performance issues may arise when views update too frequently. Observations help address this problem by ensuring that views only update when necessary. By properly updating views, we can enhance app performance and avoid lagging or hanging issues. Observations provide a more efficient way to update views based on their actual need for redrawing.
Transitioning to Observations – A Demo Project
To demonstrate the benefits of observations, we walk through a demo project that transitions from the old data flow style to the new observation style. We modify the model files, replacing the existing property wrappers with observations. The demo project showcases the improved performance achieved with the new observation style.
Model Definition and the new @Observation Macro
These are the model definitions for the old Observable protocol style with struct models:
import Foundation
struct Book: Identifiable {
var title: String
var author = Author()
var isAvailable = true
let id = UUID()
let iconIndex: Int = Int.random(in: 0...4)
}
struct Author: Identifiable {
var name = "Sample Author"
let id = UUID()
}
import SwiftUI
class Library: ObservableObject {
@Published var books: [Book] = Book.examples()
var availableBooksCount: Int {
books.filter(\.isAvailable).count
}
func delete(book: Book) {
if let index = books.firstIndex(where: { $0.id == book.id }) {
books.remove(at: index)
}
}
}
Data model definitions should change from struct to class. You should add the @Observable macro to all your model classes:
import SwiftUI
import Observation
@Observable class Book: Identifiable {
var title: String
var author = Author()
var isAvailable = true
let id = UUID()
let iconIndex: Int = Int.random(in: 0...4)
init(title: String) {
self.title = title
}
}
@Observable class Author: Identifiable {
var name: String
let id = UUID()
init(name: String = "Sample Author") {
self.name = name
}
}
@Observable class Library {
var books: [Book] = Book.examples()
var availableBooksCount: Int {
books.filter(\.isAvailable).count
}
func delete(book: Book) {
if let index = books.firstIndex(where: { $0.id == book.id }) {
books.remove(at: index)
}
}
}
Because I am now using classes, I need to write my own custom initialisers.
Defining an Environment object with Observation
You can pass observables in the environment if you want easily pass it through out your whole application. I am defining an EnvironmentValue for the ´Library´ class like so:
extension EnvironmentValues {
var library: Library {
get { self[LibraryKey.self] }
set { self[LibraryKey.self] = newValue }
}
}
private struct LibraryKey: EnvironmentKey {
static var defaultValue: Library = Library()
}
This allows me to replace all instances of EnvironmentObjects with the following code. The main app will create a new instance of ´Library´ and inject it in the environment like so:
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(\.library, library)
}
}
}
Now I can access the ´Library´ instance from anywhere in the app. For example, in ´LibraryView´ I can use the book array like so:
import SwiftUI
struct LibraryView: View {
@Environment(\.library) private var library
var body: some View {
NavigationView {
List(library.books) { book in
NavigationLink {
BookView(book: book)
} label: {
LibraryItemView(book: book,
imageName: library.iconName(for: book))
}
}
.navigationTitle("Observation")
.toolbar(content: {
Text("Books available: \(library.availableBooksCount)")
})
}
}
}
#Preview {
LibraryView()
.environment(\.library, Library())
}
Converting from @StateObject to @State
ObservableObject view models have to be owned by views with the help of ´@StateObject´ property wrapper:
import SwiftUI
@main
struct BookReaderApp: App {
@StateObject private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environmentObject(library)
}
}
}
With the new Observation you can simply replace ´@StateObject´ with ´@State´:
import SwiftUI
@main
struct BookReaderApp: App {
@State private var library = Library()
var body: some Scene {
WindowGroup {
LibraryView()
.environment(\.library, library)
}
}
}
Passing Bindings with Observation in SwiftUI
I have in my ´BookEditView´ a ´@Binding´ for book:
import SwiftUI
struct BookEditView: View {
@Binding var book: Book
var body: some View {
...
TextField("Title", text: $book.title)
...
}
}
#Preview {
BookEditView(book: .constant(Book(title: "title")))
}
This is necessary to allow SwiftUI views like TextField and Toggle to create a binding to data properties.
In Observation, instead of ´@Binding´ we have to use ´@Bindable´.
import SwiftUI
struct BookEditView: View {
@Bindable var book: Book
@Environment(\.dismiss) private var dismiss
var body: some View {
VStack() {
HStack {
Text("Title")
TextField("Title", text: $book.title)
.textFieldStyle(.roundedBorder)
.onSubmit {
dismiss()
}
}
Toggle(isOn: $book.isAvailable) {
Text("Book is available")
}
Button {
book.isAvailable = false
} label: {
Text("set unaba")
}
Button("Close") {
dismiss()
}
.buttonStyle(.borderedProminent)
}
.padding()
}
}
#Preview {
BookEditView(book: Book(title: "title"))
}
Conclusion
Observations offer a powerful solution for simplifying data flow and improving app performance in Swift UI. By adopting observations, developers can streamline their code, enhance performance, and deliver a smoother user experience. We encourage developers to explore and adopt the new observation features in their projects. Stay tuned for more content on Swift data and data flow.
Further Reading:
Thanks for the cool article, however, I believe that you don’t need to mark the Library model with @Observable as long as its properties are @Observable.
And I have a question: If I want to fully adopt the new Observation framework by embracing State, Bindable and Environment wrappers and discard all other property wrappers (like Binding, although I can still use them as they’re not deprecated yet), how do I migrate the following view to the Observation framework in order to be able to do changes on an array of Bindable objects:
“`
struct MyView: View {
@Binding var books: [Book]
var body: some View {
Button(“Add Book”) {
books.append(Book())
}
}
}
“`
I couldn’t find a way to add a book to the array without @Binding property wrapper.
You don’t need to import the Observation framework as it comes with SwiftUI