SwiftUI Programming: ObjectBinding and BindableObject

Jan 10, 2020 · 9 mins read · Costantino Pistagna · @valv0

SwiftUI Programming: ObjectBinding and BindableObject

In a previous article we have seen that in SwiftUI is possible to associate one or more states to a View through the modifier @State; the latter must be used with value types allocated within the declaring View. This way our application is able to autonomously understand when to update the user interface with respect to the changes made to our variable marked with @State which, we recall, is identified as a source of truth.

But what if our source of truth is a complex object designed, for example, as a class construct? In this case @State will fail in its intent because it is designed to work exclusively with local properties of our View.

SwiftUI provides a better further concept to dialogue with that case: @ObjectBinding. The latter could be used in all cases in which the property is passed by reference such as in the class case.


struct MyCustomObject : View {
    @ObjectBinding var myObject
    var body: some View {
        Text(myObject.someTextProperty)
    }
}



BindableObject protocol

Properties with an associated @ObjectBinding modifier, therefore, refer to class objects. In order for a class to be compatible with the @ObjectBinding modifier, the BindableObject protocol must be adopted.

BindableObject protocol is fairly simple in its simpliest variant and requires only that our class implement a property called didChange, which will notify the subscribed View objects so that they can react correctly to visual interface changes.


class MyObject : BindableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    var someTextProperty:String = "" {
        didSet {
            didChange.send( () )
        }
    }
    
    ...
}



Combine framework and PassthroughSubject

In order to work, the code above needs to include the support for the Combine framework at the beginning of our swift file.


import Combine

Combine framework is a very powerful and somewhat complex tool that we will see in a future article. For now we just need to know that it - among many other things - provides a class called PassthroughSubject (our property didChange is an instance of this class). It is responsible to notify (Publishing) the interested parties (Subscribers) that a specific event happened.

PassthroughSubject, as the name suggests, behaves very similarly to traditional notifications made available by the Swift Foundation framework with NSNotificationCenter: the object specified as a parameter (in our case Void, since we don’t want to pass any parameter) is passed roughly and without any modification to the recipient along with a notification event that trigger a new rebuild of the view.

If you want to specify an error along with the notification, you can pass it as a second parameter. In our example we are not interested in this eventuality and it is the reason why we pass Never as the second parameter for the PasstroughSubject invocation.


let didChange = PassthroughSubject<Void, Never>()

Once an instance of type PassthroughSubject is created, it could be used to perform the publishing of our event whenever we wish to notify subscribed Views about changes in our source of truth.

To accomplish the last requirement we use the didSet construct made available by Swift which is invoked whenever the value of a propery changes:


var someTextProperty:String = "" {
    didSet {
        didChange.send( () )
    }
}

This way, when we make a change to the someTextProperty the didSet construct will be invoked. Inside it we will make a call to the send() method of the PassthroughSubject instance.Notice how we specify a Void type parameter by passing a pair of parentheses () within the method.

Doing so all objects subscribed to changes - those that have declared their property as @ObjectBinding - will know that the time has come to refresh their body.

Fig.1 - Simple schema illustrating references with ObjectBinding.
Fig.1 - Simple schema illustrating references with ObjectBinding.

As you can see from the diagram above, our model is characterized by a single instance in memory, through a class compliant with the BindableObject protocol. Our View objects contain a reference to the original object.

This way a characteristic problem known as the duplication of the source of truth is avoided: changes to the model will be unique and characterized by an immediate update of all the views that refer our class.

Fig.2 - Simple schema illustrating source of truth and ObjectBinding.
Fig.2 - Simple schema illustrating source of truth and ObjectBinding.



An example

Let’s try to put the above concepts into practice with an example. In the previous article we had configured our CustomTableViewCell to use a variable passed by value containing a struct named Person.


struct CustomTableViewCell : View {
    var model: Person
    
    var body: some View {
    ...

This approach didn’t allow the CustomTableViewCell to be independent regarding updates of its own content; The variable, in fact, was copied during the CustomTableViewCell init phase.

Every change to its content was managed through a @State variable, owned by ContentView that regenerate the new cell with the new content each time a change happen.

Fig.3 - State vs Object binding reference.
Fig.3 - State vs Object binding reference.

Through the use of the @ObjectBinding concept, we can make unique the Person model by passing just a reference to the CustomTableViewCell, as shown in the graph above on the right.

The first thing to do is modify our Person model to use a class that conforms to the BindableObject protocol instead of a struct.


class Person : BindableObject {
    let didChange = PassthroughSubject<Void, Never>()
    
    var photoName:String { didSet { update() } }
    var title:String { didSet { update() } }
    var subtitle:String { didSet { update() } }
    
    init(photoName: String, title: String, subtitle: String) {
        self.photoName = photoName
        self.title = title
        self.subtitle = subtitle
    }
    
    func update() {
        didChange.send(())
    }
}

Our new model adopts the BindableObject protocol by implementing the +didChange** property through a PassthroughSubject *instance.

Each property affected by possible changes to the visual interface ( photoName, title and subtitle ) has been adequately modified to react through the didSet construct. This way, whenever we update one of the variables, the update() function will be triggered by sending, in turn, a notification to all subscribers through the PassthroughSubject instance.

Finally, CustomTableViewCell will be modified to take advantage of the new reference model. To do this we simply prepend the @ObjectBinding modifier discussed above.


struct CustomTableCell : View {
    @ObjectBinding var model: Person
    
    var body: some View {
        return HStack {
            Image(model.photoName)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .clipShape(Circle())
                .frame(width: 60, height: 60, alignment: .center)
            VStack(alignment: .leading) {
                Text(model.title).font(.title)
                Text(model.subtitle).font(.subheadline).fontWeight(.thin)
            }
            Spacer()
        }
    }
}

This way the CustomTableViewCell does not have a copy of the Person model. Instead it works with a reference to the memory area in which the original model is stored. Let’s see where and how to create the model to be passed as a reference.

ContentView will be responsible for such requirement, creating the actual model in memory and by passing it by reference to the CustomTableViewCell.


struct ContentView : View {
    var dataSource:[Person] = [
        Person(photoName: "photoThumb", title: "Mario Rossi", subtitle: "Amministratore"),
        Person(photoName: "photoThumb", title: "Giuseppe Bianchi", subtitle: "Consigliere"),
        Person(photoName: "photoThumb", title: "Luca Verdi", subtitle: "Funzionario"),
    ]
    var body: some View {
        List {
            CustomTableCell(model: dataSource[0])
            CustomTableCell(model: dataSource[1])
            CustomTableCell(model: dataSource[2])
        }
    }
}



List and TableView

This new version of ContentView uses a new object called List which groups elements pretty much the VStack counterpart. The difference is in the visualization: the result will be much like an UITableView from the counterpart UIKit framework

What we see above is List simplest form of declaration. In the next article we will see how to make its content dynamic, based on the declared model.



–See you soon!

Costantino Pistagna
Costantino Pistagna · @valv0 Costantino is a software architect, project manager and consultant with more than ten years of experience in the software industry. He developed and managed projects for universities, medium-sized companies, multi-national corporations, and startups. He is among the first teachers for the Apple's iOS Developer Academy, based in Europe. Regularly, he lectures iOS development around the world, giving students the skills to develop their own high quality apps. While not writing apps, Costantino improves his chefs skills, travelling the world with his beautiful family.