RxSwift: How to make your favorite Delegate-based-APIs Reactive ?

If you are an iOS or macOS developer using Swift, you must be familiar with protocol conformance in order to hand off responsibilities to another class or structure. The Delegate Pattern is found all over Foundation, AppKit and UIKit which are the core frameworks for iOS and macOS development.
Such as UICollectionViewDelegate or URLSessionDelegate.
In order to prevent you from writing the boilerplate code of Delegate APIs, let’s make them Reactive using RxSwift.

The source code of this article is available here:

RxSwift version used: 4.3.1

MKLocalSearchCompleterDelegate presentation

If you’ve never used MapKit or any MKLocalSearchCompleter in an application, this is a quite new API (since iOS 9.3) allowing you to get autocomplete address results based on an incomplete query request which is a simple String. The query string is passed through the MKLocalSearchCompleter’s property queryFragment.

The MKLocalSearchCompleterDelegate protocol is quite simple. It’s made of two optional methods
successCallback

This one is called whenever the parent MKLocalSearchCompleter results Array gets updated. This way you are always aware of the most recent results based on the last queryFragment you passed to the parent MKLocalSearchCompleter’s object.
The results Array is available through the completer’s readonly results property which is an Array of MKLocalSearchCompletion objects.
failCallback

This one is the error callback called when the parent’s MKLocalSearchCompleter is unable to generate a list of search results. This way, you can handle the error and let your users know what and why it went wrong.

The concrete RxCocoa’s DelegateProxy: RxMKLocalSearchCompleterDelegateProxy

Here we are building a concrete DelegateProxy type that will be used by the upcoming MKLocalSearchCompleter’s Reactive extension.

First we need to make MKLocaSearchCompleter conform to the RxCocoa HasDelegate protocol. We can achieve this with a simple extension.
HasDelegate

Here we are setting the HasDelegate’s associated type Delegate to the concrete type MKLocaSearchCompleterDelegate.

This allows us to use the RxCocoa’s DelegateProxyType delegate setter and getter default implementation through the constraint extension of the DelegateProxyType. Here is the extension mentioned:
DelegateProxy-implementation
So whenever the DelegateProxyType’s ParentObject (associatedType) implements HasDelegate and DelegateProxyType’s Delegate (associatedType) is the same type as ParentObject.Delegate (from HasDelegate protocol) you get the above two methods default implementation for free.

Next we have to create a concrete RxMKLocalSearchCompleterDelegateProxy class that inherits from RxCocoa’s DelegateProxy<ParentObject, Delegate> class. It also needs to implement RxCocoa’s DelegateProxyType protocol as well as the MKLocalSearchCompleterDelegate protocol.

Here ParentObject will be of type MKLocalSearchCompleter and Delegate will be of type MKLocalSearchCompleterDelegate.
RxMKLocalSearchCompleterDelegateProxy

The localSearchCompleter is declared as weak to prevent retain cycles. It is readonly too.

The init method stores the given localSearchCompleter and then initializes the current object using RxCocoa’s superclass DelegateProxy<ParentObject, Delegate> initializer.

In the registerKnownImplementations() method we call the superclass method register<Parent> (its signature is below). In the “make” escaping closure we then return the RxMKLocalSearchCompleterDelegateProxy initializer method with the MKLocaSearchCompleter type as a parameter.
register

The “register” method is called by RxCocoa’s DelegateProxyType in order to store our proxy subclass to its own proxy factory.
Now that RxMKLocalSearchCompleterDelegateProxy is ready, let’s make MKLocalSearchCompleter reactive 🚀.

Rx All the things 🙌🏻

The goal is to make MKLocalSearchCompleter reactive. To do so, we are going to make an extension to the RxSwift’s Reactive type, constraining its Base type to be MKLocalSearchCompleter.
MKLocalSearchCompleterBase

Then we will need to declare a delegate property inside this extension which is of type DelegateProxy<MKLocalSearchCompleter, MKLocalSearchCompleterDelegate>.
MKLocalSearchCompleterBase-Delegate

This delegate property is a computed property initialized by the previously created RxMKLocalSearchCompleterDelegateProxy class using this method:
carbon

You must always call this method to instantiate a DelegateProxyType object instead of directly instantiating RxMKLocalSearchCompleterDelegateProxy through its initializer.

Indeed, this method takes care of creating and installing a proxy instance and then returns it or returns an existing proxy instance already installed.

didUpdateResults

The last task is to return observable sequences for each of the two MKLocalSearchCompleterDelegate’s methods. First let’s see how we do it to recover the last updated MKLocalSearchCompleter results using a computed property.
didUpdateResults

We use the delegate proxy using its methodInvoked() passing the Selector of the corresponding delegate method to let the delegate proxy observe calls to this method returning results as an observable sequence.
We then map the result of methodInvoked() which is an Array of Any representing the list of the delegate method parameter(s). In this case there is only one parameter of type MKLocalSearchCompleter so the focus is made on the first parameter (a[0]).

You must be wondering what is the castOrThrow(...) method 🤔. This is a method used all over RxCocoa, but not exposed outside of the RxSwift library. This simply tries to cast the given object (2nd parameter) into the given type (1st parameter) or throws a RxCocoaError if it fails. Here is the implementation found in RxCocoa. You can add this to your file and make it public, fileprivate or private so it fits your needs.
castOrThrow

The didUpdateResults computed property then returns a ControlEvent of MKLocalSearchCompleter. The ControlEvent type is used to provide a safe representation of the observable sequence that represent UI events. The main properties of ControlEvent sequences are:

  • It never fails
  • It won’t send any initial value on subscription
  • It will Complete sequence on control being deallocated
  • It never errors out
  • It delivers events on the MainScheduler.instance

This sequence type is appropriate to what we need here because updated MKLocalSearchCompleter.results will be propagated to any UI element such as UITableView presenting the results to the user while he is typing his location query.

didFailWithError

Let’s see how the failure delegate method is handled into an observable sequence through the didFailWithError computed property.
didFailWithError

This is really similar to the previous one, I’ll only address the a[1] syntax. It focuses on the second parameter of the given method invoked which is the actual Error object, at index 1 in the corresponding Array of parameters.

queryFragment

The MKLocalSearchCompleter’s queryFragment property is the entry point of the query mechanism. Whenever it’s value changes, a new request is made.
Let’s make it reactive too! So it can be bound to any text oriented UI like UITextField or UISearchBar.
queryFragment

Here it is, just like the text property of a UILabel in RxCocoa we’ve made a Binder which takes a String and affects its value to the underlying queryFragment property of the MKLocalSearchCompleter object.

Usage

As an example, we are going to bind a UITextfield’s reactive text property to a MKLocalSearchCompleter’s reactive queryFragment property.
usage

  1. Initialization of a MKLocalSearchCompleter and a UITexfield object
  2. The UITextfield’s reactive text property is unwrapped to be sure it’s not an optional String sequence with .orEmpty and then bound to our newly created reactive queryFragment property.
  3. We then subscribe to the MKLocalSearchCompleter’s reactive didUpdateResults sequence to then print the address completion results.

So now, every user’s input on the UITextfield is broadcasted to the searchCompleter.queryFragment property triggering a new address completion request. The results are automatically forwarded to the .didUpdateResults sequence allowing you to present the autocomplete list of results to your users. 👏🏻


Vous avez trouvé cette publication utile? Cliquer sur
Ippon
Ippon est un cabinet de conseil en technologies, créé en 2002 par un sportif de Haut Niveau et un polytechnicien, avec pour ambition de devenir leader sur les solutions Digitales, Cloud et BigData.

Ippon accompagne les entreprises dans le développement et la transformation de leur système d’information avec des applications performantes et des solutions robustes.

Ippon propose une offre de services à 360° pour répondre à l’ensemble des besoins en innovation technologique : Conseil, Design, Développement, Hébergement et Formation.

Nous avons réalisé, en 2017, un chiffre d’affaires de 31 M€ en croissance organique de 30%. Nous sommes aujourd’hui un groupe international riche de plus de 320 consultants répartis en France, aux USA, en Australie et au Maroc.
FRANCE Website LinkedIn