Simplifying Data Flow and Improving Performance in SwiftUI with Observation

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.

Free SwiftUI Layout Book

Get Hands-on with SwiftUI Layout.

Master SwiftUI layouts with real-world examples and step-by-step guide. Building Complex and Responsive Interfaces.

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:

3 thoughts on “Simplifying Data Flow and Improving Performance in SwiftUI with Observation”

  1. 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.

    Reply
  2. 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.

    Reply

Leave a Comment

Subscribe to My Newsletter

Want the latest iOS development trends and insights delivered to your inbox? Subscribe to our newsletter now!

Newsletter Form