EXPEDIA GROUP TECHNOLOGY — ENGINEERING

ObservableObject, @Published, and Protocols with SwiftUI, UIKit, and Cuckoo

How to use Combine and protocols in any view

Kari Grooms
Expedia Group Technology

--

Woman using coin binoculars at the Empire State Building in New York City.
Photo by Frederick Marschall on Unsplash

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.

A simple view that displays “Price not available” if we do not have any pricing data to show. A “Fetch price” button is provided to initiate a request for pricing data for demo 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 @Publishedproperty 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.

Animated gif showing that the “Fetch price” button is being tapped, but the PriceView is not displaying a new price.

@Published properties and protocols

Let’s try adding the @Publishedproperty requirement to our PriceRequesting protocol.

This image shows the PriceRequesting protocol with the @Published annotation added to the price property. Xcode is displaying an error message: “Property ‘price’ declared inside a protocol cannot have a wrapper”.

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.

Animated gif showing that the “Fetch price” button is being tapped and the displayed price is being updated.

Using an extended ObservableObject protocol in SwiftUI

Now that our protocol is defined, let’s try and use it in our SwiftUI view.

Image of the PriceView code where we changed “@ObservedObject var pricing: PricingProvider” to “@ObservedObject var pricing: PriceRequesting”. This, however, causes errors: “Protocol ‘PriceRequesting’ as a type cannot conform to ‘ObservableObject’” and “Protocol ‘PriceRequesting’ can only be used as a generic constraint because it has Self or associated type requirements”.

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.

Animated gif showing that the “Fetch price” button is being tapped and the displayed price is being updated after implementing the PriceRequesting protocol via a @State and .onReceive(_:) implementation.

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.

Animated gif of the UIKit version of the same pricing view. Shows that when the “Fetch price” button is being tapped, the displayed price is being updated.
UIKit version

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.

Image of Xcode showing the MockPriceRequesting class generated by Cuckoo with errors: “Protocol ‘PriceRequesting’ can on be used a generic constraint because it has Self or associated type requirements”.

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.

Image of Xcode showing the PriceRequestingStub class generated by Cuckoo with errors: “Cannot specialize a non-generic definition”.

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. Use AnyPublisher or Published<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 with where clauses.
  • You can bypass extending ObservableObject altogether and just require an AnyPublisher 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 use AnyPublisher.

Thanks for reading. 😄

Learn more about technology at Expedia Group

--

--