Implementing a generic search screen using MVVM and RxSwift
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 :
- Has a laid-out
UISearchBar
. - Handles the different states of the view controller: results, loading, no results, other error.
- Declare view dependencies for subclasses to override accordingly.
- A generic view model class that:
- Exposes an
Observer
instance for us to bind to the search bar events. - Exposes
Driver
instances emitting events for the different states (loading, results, etc). - 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).
- Handles search bar events, by debouncing and filtering repeated values.
The Implementation
Dependencies
We’ll need RxSwift
and RxCocoa
for 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:
- We start using our
searchSubject
as our event search input. We filter out empty queries, discard repeated values and use thedebounce
operator to only trigger a request after the user has finished entering the search term. - 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. - Every time we start a new search request, we will clear the error state by emitting a
.next(nil)
event. - We emit
.next(true)
event to the loading subject. - 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 theerrorSubject
so that we can handle it as desired from the view controller. - We set back the loading state to
false
. - 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. - Finally we add our disposable to our
DisposableBag
instance.
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 thesearchObserver
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 Dog
and 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 DogSearchViewController
class.
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.