Implementing a generic search screen using MVVM and RxSwift

Andrés Portillo
6 min readDec 17, 2018

--

As iOS developers, one of the tasks that we end up working on again and again is implementing a search screen. If our app requires us to add one search screen, chances are we are gonna need another one for a different feature of our app, so it might be worth it investing in a generic solution. In this blog post, I wanna propose a solution that uses the MVVM design pattern with RxSwift. This generic solution will provide us with:

  • A generic UIViewController subclass that :
  1. Has a laid-out UISearchBar.
  2. Handles the different states of the view controller: results, loading, no results, other error.
  3. Declare view dependencies for subclasses to override accordingly.
  • A generic view model class that:
  1. Exposes an Observerinstance for us to bind to the search bar events.
  2. Exposes Driverinstances emitting events for the different states (loading, results, etc).
  3. Handles subsequent events so that we only process results for our last query (As soon as a new request is sent, we discard the previous one).
  4. Handles search bar events, by debouncing and filtering repeated values.

The Implementation

Dependencies

We’ll need RxSwiftand RxCocoafor our implementation.

The View Model

Since we are using MVVM, our view model is going to handle all the logic we need for the view, in this case our SearchViewController. For this we are going to define our inputs (observers) and outputs (drivers). So let's start by defining our generic class with this properties:

We have defined our inputs and outputs using PublishSubject instances which we respectively expose as drivers. Now we need to strategically process our input and transform the signal into outputs (for every valid search input, we should asynchronously return search results). So where are these search results coming from? Remember that our generic class is designed to be subclassed; we can define a method whose implementation will be a requirement for subclasses.

This is a nice trick since now we can use this method to model all our transformations just as if the method was already implemented. Let’s go ahead and connect the dots of our implementation.

Let’s break down what we are doing here:

  1. We start using our searchSubject as our event search input. We filter out empty queries, discard repeated values and use the debounce operator to only trigger a request after the user has finished entering the search term.
  2. We use the flapMapLatest operator to discard search results we are not longer interested in. As soon as a new valid is entered in the search bar, a new observable will be created and the old one (if there was a pending search request) will be overwritten.
  3. Every time we start a new search request, we will clear the error state by emitting a .next(nil) event.
  4. We emit .next(true) event to the loading subject.
  5. Here we call our function (that still needs to be overridden) passing the new search term. Normally, if our search errors, our observable would emit a complete event and our observation would be disposed. Since this is an undesired behaviour, we are catching the error and forwarding it via the errorSubject so that we can handle it as desired from the view controller.
  6. We set back the loading state to false.
  7. Here we check the results we’ve gotten for our search; if we didn’t get any results, we’ll emit a custom error SearchError.notFound. This will allow us to handle empty states and other errors with a single view in our view controller.
  8. Finally we add our disposable to our DisposableBaginstance.

With our view model ready to work, let’s create our accompanying view controller.

There are a few things to comment on here:

  • We are defining 3 different subviews for our view controller open to be overriden, to keep things flexible we are only requiring subclasses to override the contentView. The loading and error views stay optional.
  • We need to initialise the viewController with a view model of type SearchViewModel<T>, where the generic value T has to match the one of our view controller.
  • The only view we are laying our here is the searchBar, the rest of the views have to be laid-out as desired by subclasses.
  • I am using Cartographyto add constraints, which offers a very nice DSL on top of Auto-layout.
  • In our viewDidLoad , we are hiding our loading and error views as the initial state.

Finally we need to use the sources from our view model to handle the different states of our view. When we fire a request, we should hide the content view and show the loading one, similarly, we should hide the loading view once the request response arrives, and depending on this response, we switch to the error or content view. Let’s add a method for this

Let’s go over the code in this method:

  • First of all we are connecting our UISearchBar to our view model by binding its text events to the searchObserver property.
  • Then we are handling the hidden state for every one of our views separately. Notice that we only create bindings for the loading and error view if the subclass has actually defined them.

and that’s it!

We have a generic view controller that handles loading and error states. Now let’s go ahead and create a demo app to see how we might use these classes.

Demo

Let’s create a DogSearchViewController class that will subclass SearchViewController, specialise it using a model class Dogand require a view model of type SearchViewModel<Dog>.

Dog Search View Model

In our view model, all we need to do is implement the search method. Normally here is where we would hit an endpoint and asynchronously ask for our dogs; for the purpose of this demo we are only going to simulate a search functionality by returning dogs whose name start with the same letter as the term. If we don’t find a dog for the term, will return SearchError.notFound. Finally, we are going to implement our DogSearchViewControllerclass.

DogSearchViewController

Although it seems like a lot of code, 95% of it is layout code. We are creating views for the error and loading states, we are laying them out and we are registering the cell we are going to use to show results. The relevant part here is in the setupObservers method, here we are binding the content driver to our table view, refreshing the content of the tableView every time new results are being generated. We can do with our content driver whatever we want, we might use it to feed a UICollectionView or any other custom view. Here is how our demo looks like:

loading state.

Results starting with ‘s’

Error state

This is all for today!feel free to drop any comments on Twitter @andresportillos if you have any questions or ideas.

Originally published at andresportillo.de on December 16, 2018.

--

--