When Apple introduced SwiftUI in WWDC 2019, they took care how to onboard developers with the new technology in their ecosystem. One of the important topics was how the data flows in the SwiftUI app, because it’s very different from a traditional UIKit app. The main difference you ask?

  • being declarative.

The framework

SwiftUI is the shortest path to a great app […]

Luca Bernardi @ WWDC19’

Yes, SwiftUI is an Apple’s declaritive approach syntax framework for building apps for iOS, tvOS, macOS and watchOS. I won’t go into the details about the framework, how it’s built etc. so, if you want to read more about the framework itself go here.

short example of a declaritive syntax SwiftUI view:

struct Person {
    let avatar: String
    var name: String
    var surname: String
    var isFavourite: Bool
}

struct PersonView: View {
    @State var person: Person

    var body: some View {
        HStack {
            Image(person.avatar)
            VStack(alignment: .leading) {
                Text(person.name)
                Text(person.surname)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

Looks pretty easy and fast to build a PersonView that has a model connected to it, without a ViewController, constraints for autolayout (or using a storyboard - I assume you don’t use it ;) ), viewDidLoad, custom passing a model to it and etc.

Data flow

Let’s start with some basics about how the data is passed around in a SwiftUI view. One of the most important things is that the View should have single source of truth. Avoid duplication of view’s state.

Quick cheat sheet

Quick cheat sheet reference for SwiftUI objects.

Quick cheat sheet reference for SwiftUI objects.

@State

SwiftUI uses property wrappers, to augment the property behaviour when it’s read or written to. First of them is a local state of view:

@State private var name: String = "James"
@State private var age: Int = 42

Every time, the @State property changes, it notifies the view that is owning this source of truth and re-renders the view with the new data. It stores the value, tracks it and makes a dependency towards a view, so the framework knows when it changed and to what value, so it can trigger a view to re-build itself. The data is persisted between the view render passes.

Hint: it’s a good practise to mark the @State as a private property to enforce that the data is owned and managed by the view where it was created.

@StateObject

The @StateObject was introduced in WWDC 2020, to have an easy way to have a local state of view, but for a reference type. This is very analagic to the @State property wrapper. It makes the referenced object alive between render passes of the SwiftUI view, so it’s not deallocated when the view is re-rendered and the data of the state is persisted. The object that is referenced by @StateObject needs only to be implementing the protocol of ObservableObject. Let’s see a quick example:

@MainActor
class PersonAvatar: ObservableObject {
    @Published var image: UIImage = UIImage(named: "placeholder") ?? UIImage()
    private let path: String = "https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=1260&h=750&dpr=2"
    
    func fetchAvatar() async {
        let url = URL(string: path)!
        let urlRequest = URLRequest(url: url)
        let (data, _) = try! await URLSession.shared.data(for: urlRequest)
        self.image = UIImage(data: data)!
    }
}

struct AvatarView: View {
    @StateObject private var avatar = PersonAvatar()
    
    var body: some View {
        VStack {
            Image(uiImage: avatar.image)
        }
        .task {
            await avatar.fetchAvatar()
        }
    }
}

On the first run, the SwiftUI view will get rendered with image of a placeholder file and done. When the view is built, the asynchronous task starts and run in the background. When the task finishes it updates the @Published var image: UIImage with a jsut downloaded image and the SwiftUI view gets informed about the change through the publisher. Second render pass is being made with the same PersonAvatar object without instantiating a new one. Quite smart huh? Let’s move on.

Hint: The @StateObject defined in the view is it’s source of truth, please remember to not duplicate the same object in your view hierarchy in order to avoid multiple sources of truth.

@Binding

When we have a situation in which the source of truth is not owned by the view, we can use @Binding property wrapper. We got the PersonView SwiftUI view example above. If we would like to have a subview where we can edit the name, surname or a favorite contact we should use @Binding property wrapper to update that model. Let’s see an example:

struct EditPersonDetailsView: View {
    @Binding var person: Person
    
    var body: some View {
        VStack(alignment: .leading) {
            TextField("Name", text: $person.name)
            TextField("Surname", text: $person.surname)
            
            Button {
                person.isFavourite.toggle()
            } label: {
                Text("Favourite: \(person.isFavourite ? "yes" : "no")")
            }
        }
    }
}

As you can see above, the SwiftUI view is able to modify the data model of a person, but it’s not owning it. There’s no initialization of the property, because it’s expected to be injected into the EditPersonDetailsView at view initialization phase. The TextFields will modify the name name surname and the button will update the boolean of the isFavourite. Assuming, that the EditPersonDetailsView was presented as a modal for example, when we will close it the Person data in the PersonView will be updated automatically for you. That’s the @Binding magic. It creates a two directional way of read/write the data from the model passed between two views.

@ObservedObject

This property wrapper @ObservedObject can be used to observe (as the name suggests) an object that was created somewhere else in the view hierarchy or even outside and our view listens for changes or can introduce some changes into it. The main difference of ObservedObject and @StateObject is that the view doesn’t own the data/state when using the @ObservedObject property.

An example with a person model using reference type and a variant of the person’s detail view:

class PersonModel: ObservableObject {
    @Published var avatar: String
    @Published var name: String
    @Published var surname: String
    
    init(avatar: String, name: String, surname: String) {
        self.avatar = avatar
        self.name = name
        self.surname = surname
    }
}

struct PersonDetailViewObservedObject: View {
    @ObservedObject var person: PersonModel

    var body: some View {
        HStack {
            Image(person.avatar)
            VStack(alignment: .leading) {
                Text(person.name)
                Text(person.surname)
                    .foregroundStyle(.secondary)
            }
        }
    }
}

Initializing the view somewhere in the view hierarchy requires passing the model into it:

let personModel: PersonModel = PersonModel(avatar: "avatar.png", name: "James", surname: "Bond")
// ...
PersonDetailViewObservedObject(person: personModel)

@EnvironmentObject

This property wrapper is used, when we have some model of data that can or should be shared across many views within the view hierarchy. When injected on the high level of the view hierarchy, views that are lower in the hierarchy can easily access the shared passed model by simply using:

@EnvironmentObject private var userAuth: UserAuth

in the view that needs to access it. It’s just ‘that’ simple.

Here’s a nice visual explanation of the environment from the WWDC19’ by Raj Ramamurthy:

Q@EnvironmentObject explanation slide from WWDC19' by Raj Ramamurthy.

@EnvironmentObject explanation slide from WWDC19' by Raj Ramamurthy.

QEnvironmentObject injection view hierarchy example slide from WWDC19' by Raj Ramamurthy.

@EnvironmentObject injection view hierarchy example slide from WWDC19' by Raj Ramamurthy.

ok, now let’s see an example code below:

class UserAuth: ObservableObject {
    @Published var isUserLoggedIn: Bool = false
    
    func login() {
        // some auth code
        //...
        isUserLoggedIn = true
    }
    
    func logout() {
        // drop auth keys
        isUserLoggedIn = false
    }
}

struct PersonDetailViewEnv: View {
    @Binding var person: Person
    @EnvironmentObject private var userAuth: UserAuth
    
    private func openLogin() {
        // proceed to login screen
    }
    
    var body: some View {
        HStack {
            if userAuth.isUserLoggedIn {
                Image(person.avatar)
                VStack(alignment: .leading) {
                    Text(person.name)
                    Text(person.surname)
                        .foregroundStyle(.secondary)
                }
            } else {
                Text("You have to log in.")
                Button {
                    openLogin()
                } label: {
                    Text("login")
                }

            }
        }
    }
}

What happens here is that, there is some UserAuth living in the app, that has an information about whether the user is logged in or not. It makes use of @Published property wrapper to notify all of the listening views for the state changes.

User authentication is something that should be shared in the app from the very first view in the app, so let’s put it in the MainView:

@main
struct PersonApp: App {
    let userAuth = UserAuth()
    
    var body: some Scene {
        WindowGroup {
            MainView()
                .environmentObject(userAuth)
        }
    }
}

Let’s assume that in this case, the MainView view is containing the PersonDetailViewEnv view, so the PersonDetailViewEnv doesn’t need to be invoked with .environmentObject(userAuth) because it’s already in the same chain of the view hierarchy, so the environment object is alread in place, just the PersonDetailViewEnv needs to access it by:

@EnvironmentObject private var userAuth: UserAuth

And voila! That’s how passing the data through the @EnvironmentObject works.


Thanks for reading, until next time!

resources: