EXPEDIA GROUP TECHNOLOGY — ENGINEERING
ObservableObject, @Published, and Protocols with SwiftUI, UIKit, and Cuckoo
How to use Combine and protocols in any view
ObservableObject
was introduced as part of Apple®️’s Combine framework and is foundational to data flow in SwiftUI. Extending protocols with ObservableObject
, however, is not as straightforward. In this article, we explore patterns for leveraging protocols with ObservableObject
in both SwiftUI and UIKit and the challenges encountered along the way.
ObservableObject usage in SwiftUI
Let’s take a look at the typical usage of ObservableObject
in SwiftUI. For our example, we create an ObservableObject
that handles fetching pricing data.
For our example view, we display the price, if we have it; otherwise, we display a fallback message. We provide a button to initiate the pricing request for demonstration purposes.
Defining a protocol extending ObservableObject
Let’s see how we could leverage a protocol for the PricingProvider
.
This looks good, but we are missing a very key characteristic of an ObservableObject
. We need at least one @Published
property or we need to explicitly call self.objectWillChange.send()
so that our object can notify subscribers of its updates. Without these, our views will never display an updated price, as shown below.
@Published properties and protocols
Let’s try adding the @Published
property requirement to our PriceRequesting
protocol.
Property wrappers cannot be used inside protocols, unfortunately. Without this, we cannot enforce implementation of a publisher. There are ways around this though. @Published
wraps our property with Published<Value>
, so we can expose some values for these in our protocol.
Now our view works as expected, receiving updates from PricingProvider
, while satisfying the publisher requirements of our protocol. Note that _price
exposes the Published<Price?>
value and $price
exposes the publisher.
Using an extended ObservableObject protocol in SwiftUI
Now that our protocol is defined, let’s try and use it in our SwiftUI view.
Because ObservableObject
uses Self
for its objectWillChange
property, we cannot declare it as a type directly. We can work around this though. In order to continue to leverage @ObservedObject
, we can make PriceView
a generic type.
Another option would be to decouple our protocol from ObservableObject
altogether. Since we exposed Published<Price?>.Publisher
in our protocol, we technically do not need to extend ObservableObject
.
We will, however, need to update our view to explicitly subscribe to the publisher we defined in our protocol since we can no longer use @ObservedObject
.
In order to keep our price updated in the view, we need to create a @State
variable for this data. We use .onReceive(_:perform)
to explicitly subscribe to price updates and update our internal @State
variable to trigger a re-render of our view.
Using an extended ObservableObject protocol in UIKit
Below is the UIKit implementation of our simple pricing view. As with our SwiftUI example, we will start off using the ObservableObject
implementation directly instead of our extended protocol.
Unlike SwiftUI, there is no built-in mechanism in UIKit for subscribing to updates. Since we always have to explicitly subscribe to a publisher in UIKit, it’s rather simple to update this code to leverage the PriceRequesting
protocol instead of the ObservableObject
implementation.
Mocking ObservableObject with Cuckoo
Now that we know several options for leveraging protocols via extendingObservableObject
or utilizing Published<Value>
, let’s see if they are easily mockable with Cuckoo.
Apparently, they are not. In fact, it’s not currently possible to mock protocols extending ObservableObject
in Cuckoo. At the time of writing this, there is an open issue for it. If we want to use Cuckoo to mock our protocol, we have to decouple.
Let’s try again.
It turns out this new “Cannot specialize a non-generic definition” error is due to both the Foundation and Combine frameworks having a Published
type. All of the mocks generated by Cuckoo end up in a single GeneratedMocks.swift
file, so if you are using Foundation in another mocked file, then this error will occur.
There are a couple of options for working around this. We could be explicit in referencing Combine in our protocol.
Alternatively, we could type erase our publisher via AnyPublisher
. To enforce implementation of a publisher in our protocol, we really only need to require a Publisher
property and do not need to require a property for the published value (Published<Price?>
).
Key Takeaways
@Published
property wrappers are not allowed in protocols. UseAnyPublisher
orPublished<Value>.Publisher
to enforce a publisher implementation.- Use the
_
and$
prefixes to access the value and publisher of an@Published
property. - If you extend
ObservableObject
, be prepared to make the controllers and/or views (UIKit or SwiftUI) requiring it generic withwhere
clauses. - You can bypass extending
ObservableObject
altogether and just require anAnyPublisher
in your protocol. - SwiftUI views can achieve the same functionality as
@ObservedObject
by using a combination of@State
and.onReceive(_:)
. - Unlike SwiftUI, subscribing to publishers in UIKit via a protocol or an
ObservableObject
implementation is done the same way. - If you want Cuckoo to mock your protocols, then your protocols can’t extend
ObservableObject
(yet). See Cuckoo issue. - If using Cuckoo, beware of
Published<Value>
in your protocols due to conflicts with the Foundation framework. Save yourself the trouble and useAnyPublisher
.
Thanks for reading. 😄