22

Playing around with examples out there. Found a project that had a class that was a bindableobject and it didn't give any errors. Now that Xcode 11 beta 4 is out, I'm getting the error:

Type 'UserSettings' does not conform to protocol 'BindableObject'

It has a fix button on the error which when you click on that, it adds

typealias PublisherType = <#type#>

It expects you to fill in the type.

What would the type be?

class UserSettings: BindableObject {

    let didChange = PassthroughSubject<Void, Never>()

    var score: Int = 0 {
        didSet {
            didChange.send()
        }
    }
}
ShadowDES
  • 783
  • 9
  • 20

3 Answers3

68

Beta 4 Release notes say:

The BindableObject protocol’s requirement is now willChange instead of didChange, and should now be sent before the object changes rather than after it changes. This change allows for improved coalescing of change notifications. (51580731)

You need to change your code to:

class UserSettings: BindableObject {

    let willChange = PassthroughSubject<Void, Never>()

    var score: Int = 0 {
        willSet {
            willChange.send()
        }
    }
}

In Beta 5 they change it again. This time they deprecated BindableObject all together!

BindableObject is replaced by the ObservableObject protocol from the Combine framework. (50800624)

You can manually conform to ObservableObject by defining an objectWillChange publisher that emits before the object changes. However, by default, ObservableObject automatically synthesizes objectWillChange and emits before any @Published properties change.

@ObjectBinding is replaced by @ObservedObject.

class UserSettings: ObservableObject {
    @Published var score: Int = 0
}

struct MyView: View {
    @ObservedObject var settings: UserSettings
}
kontiki
  • 37,663
  • 13
  • 111
  • 125
  • 3
    — I wonder how long you have to change the value after calling this? And, what happens if you never change the value after calling `willChange`? – jason z Jul 18 '19 at 17:45
  • 1
    @jasonz Yes, it puzzles me too. But as far as we use willSet to call willChange.send(), we should be safe. That code executes immediately before setting the value. – kontiki Jul 18 '19 at 17:56
  • Looks like the same pattern as `setNeedLayout()` in a way. Make your changes and tell the system that UI need to update later so all changes can be aggregated and avoid multiple refresh for each property set. – Ludovic Landry Jul 24 '19 at 17:55
  • @LudovicLandry, yes, but I think jasonz's concern was that unlike beta3, you now need to notify the changes before they happen, not after. – kontiki Jul 24 '19 at 18:17
  • Getting rid of the `willSet` / `willChange.send()` boilerplate using the Beta 5 `@Published` Property Wrapper eases my previous concern. – jason z Aug 01 '19 at 13:24
  • totally, but notice you can still use it in the old-fashion way: `willSet { objectWillChange.send() }`. See an example where it was needed: https://stackoverflow.com/a/57302695/7786555 – kontiki Aug 01 '19 at 13:28
  • Ya! The `@Published` eases my concern, but doesn't totally get rid of it :). – jason z Aug 01 '19 at 13:31
  • How to change "score" outside a view? The whole reason to use this is to notify for a change of the data. My data can change as a result say a web service call. I would like to update the observed value and the change to be update to the view. – thstart Aug 30 '19 at 17:43
  • @thstart You can update it in your async code, but have to make sure you do it in the main thread: `DispatchQueue.main.async { ... }`. The framework will pick it up and update the view. – kontiki Aug 30 '19 at 18:19
  • DispatchQueue.main.async { ... } is not updating it – thstart Sep 01 '19 at 05:57
  • @thstart You should probably open a separate question, as it maybe something specific to your scenario. – kontiki Sep 01 '19 at 06:45
5

in Xcode 11.X, I verify is fine in Xcode 11.2.1, 11.3.

BindableObject is changed to ObservableObject.

ObjectBinding is now ObservedObject.

didChange should be changed to objectWillChange.

List(dataSource.pictures, id: .self) { }

You can also now get rid of the did/willChange publisher and the .send code and just make pictures @Published

The rest will be autogenerated for you.

for example:

import SwiftUI
import Combine
import Foundation

class RoomStore: ObservableObject {
    @Published var rooms: [Room]
    init(rooms: [Room]) {
        self.rooms = rooms
    }
}

struct ContentView: View {
    @ObservedObject var store = RoomStore(rooms: [])
}

ref: https://www.reddit.com/r/swift/comments/cu8cqk/getting_the_errors_pictured_below_when_try_to/

Zgpeace
  • 3,927
  • 33
  • 31
3

SwiftUI and Combine are two new frameworks that were announced at WWDC 2019. These two frameworks received a lot of attention at WWDC 2019, as evidenced by the number of sessions in which these technologies were featured.

SwiftUI was introduced as

a revolutionary, new way to build better apps, faster.

Combine is described as

a unified declarative framework for processing values over time

Between the initial release and now (May, 2020, Swift 5.2), there have been some changes. Anyone new to SwiftUI and Combine, who may have watched the WWDC videos, may be left with a few questions as to how the two frameworks work together.

Combine defines two interfaces: Publisher and Subscriber. A publisher sends events to subscribers. See sequence diagram below.

sequence diagram for combine

If you start an application in SwiftUI, and then add combine, there will be no mention of a Publisher or a Subscriber, the two main players required to use Combine. Consider this very simple sample application below.

import SwiftUI
import Combine
import SwiftUI

final class ActorViewModel: ObservableObject {
    var name : String
    private var imageUrl : URL?
    //@Published
    private (set) var image : Image = Image(systemName: "photo") {
        willSet {
            DispatchQueue.main.async {
                self.objectWillChange.send()
            }
        }
    }

    init(name: String, imageUrl: URL?) {
        self.name = name
        self.imageUrl = imageUrl
        self.fetchImage()
    }

    private func fetchImage() {
        guard nil != self.imageUrl,
            String() != self.imageUrl!.absoluteString else { return }
        let task = URLSession.shared.dataTask(with: self.imageUrl!) { (data, response, error) in

            guard nil == error , nil != response, nil != data,
                let uiImage = UIImage(data: data!) else { return }
                self.image = Image(uiImage: uiImage)

        }
        task.resume()
    }
}

struct ContentView: View {
    @ObservedObject var actor : ActorViewModel

    var body: some View {
        HStack {
            actor.image
                .resizable()
                .aspectRatio(contentMode: ContentMode.fit)
                .frame(width: 60, height: 60)
            Text(actor.name)
        }

    }
}

struct ContentView_Previews: PreviewProvider {
    static var previews: some View {
        let actor = ActorViewModel(name: "Mark Hammill",
                                   imageUrl: URL(string: "https://m.media-amazon.com/images/M/MV5BOGY2MjI5MDQtOThmMC00ZGIwLWFmYjgtYWU4MzcxOGEwMGVkXkEyXkFqcGdeQXVyMzM4MjM0Nzg@._V1_.jpg"))
        return ContentView(actor: actor)
    }
}

The app preview via the canvas will look like this:

app preview

The app uses a list view to display names and images of actors. There are just two classes to consider:

  1. ContentView -- the SwiftUI View subclass
  2. ActorViewModel -- the source of the data for the ContentView (called a ViewModel as it performs the role of VM in MVVM)

The view has a reference to the actor object, as per the class diagram below.

application class diagram

Although this example is using Combine, it is not immediately apparent. There is no mention of a Publisher or a Subscriber. What is going on?

Answer: Looking at the class hierarchy fills in the missing gaps. The below class diagram explains the full picture (click on the image to see it in greater detail).

combine UML

Consulting Apple's documentation provides definitions for these types:

  • ObservedObject: A property wrapper type that subscribes to an observable object and invalidates a view whenever the observable object changes.
  • ObservableObject: A type of object with a publisher that emits before the object has changed. By default an ObservableObject synthesizes an objectWillChange publisher that emits the changed value before any of its @Published properties changes.
  • objectWillChange: A publisher that emits before the object has changed.
  • PassthroughSubject: A subject that broadcasts elements to downstream subscribers. As a concrete implementation of Subject, the PassthroughSubject provides a convenient way to adapt existing imperative code to the Combine model.

First, consider what the @ObservedObject means. This is a property wrapper. A property wrapper reduces code duplication, and allows for a succinct syntax when declaring properties that hides how the property is stored and defined. In this case, the "Observed Object" is a property which observes another object.

In other words, the property is a Subscriber (from the Combine Framework). The actor is (through the use of a property wrapper) is a Subscriber, which subscribes to a Publisher, but what is the Publisher in this scenario?

The "Observable Object" is not itself the publisher, but rather has a publisher. The ActorViewModel conforms to the ObservableObject protocol. By doing so, it is provided with a publisher property called objectWillChange by an extension (which the framework provides on the ObservableObject protocol). This objectWillChange property is of type PassthroughSubject, which is a concrete type of the Publisher protocol. The passthrough subject has a property called send, which is a publisher method used to send data to any subscribers. So the property called "objectWillChange" is the Publisher.

To recap, the Subscriber is the property called actor from the ContentView class, and the Publisher is the property objectWillChange from the ActorViewModel class. What about the need for the Subscriber to Subscribe to the Publisher? The "@ObservedObject" property wrapper is itself a Subscriber, so it must subscribe to the Publisher. But how does the View find out about changes sent to the Subscriber? That is handled by the SwiftUI framework, which we never see.

Take-away: we don't need to worry about subscribing the view to the Publisher. On the other hand, we do need to worry about making sure the publisher tell the subscriber when something is about to change. When the image has been fetched from a remote server, and the data has been transformed into an image object, we call objectWillChange.send() to inform the View. Once the subscriber receives notification from the publisher that something is about to / has changed, it invalidates the view (which results in the view redrawing itself).

Summary The way in which SwiftUI uses a ObservedObject PropertyWrapper does not on the surface give away the fact that Combine even exists in the equation. But by inspecting ObservedObject and ObservableObject, the underlying Combine framework is revealed, along with the design pattern:

subscriber --> subscribing to a publisher --> which then publishes changes --> that are received by the subscriber

References:

Jason Cross
  • 1,721
  • 16
  • 11