Written by Maciej Walczyński
Published March 22, 2016

How to build an incremental search using FRP

Functional Reactive Programming (FRP) has changed how I think about solving problems in iOS. Here I will show you a complete and simple way to build an incremental search using FRP.

MA collageAt the beginning of the month realm.io published The Guide to FRP on iOS. It’s a great guide which eases the FRP high learning curve. I’d like to build on those talks and give some common working scenarios which I reuse in my projects.

I started programming in FRP about 2 years ago with ReactiveCocoa and it has changed the way I think about solving problems in iOS. Complex problems have become relatively easy to grasp, or at least easy to isolate to create simple solutions. They are simple once you start thinking in FRP. The learning curve is steep, but the apple is hanging really low thanks to guys like Ash Furrow and Justin Spahr-Summers. So my advice is to grab it and have a bite! The community is amazing and growing, it is NOT a premature technology.

A side note about the learning curve: I’ve hit the wall quite a few times with FRP, but always managed to get on track with a full bag of new enlightening experience. It’s really an awesome feeling to figure out your brain has got it right. What’s more, the solution is stateless, you code less, error handling is straightforward and concurrency is out of the box. Oh my! Thats a lot of benefits in practice resulting in avoided bugs. “Simplicity is a prerequisite for reliability”

I’ll show you a complete and simple solution to a problem which you probably tackled in quite a few projects. And just to brag about FRP a little more – it’s a cross-platform solution, and will work whether you are programming in Swift, JS, .NET or Java. FRP is like maths. Whether you are Swedish or French, you can still read it and enjoy it the same way. If you happen to change your dev environment from mobile to backend, you have a tool you can use throughout

The big search

Please raise your hand if you haven’t implemented a search feature at least once in your app/product. How many hands raised? 🙂

Whether it’s a search for a customer in a local database of a CRM app, a search for a city in a Weather app, or a search for food in a Weight Loss app, it’s an important feature in the app. And it almost never seems time-consuming at the beginning, but turns out underestimated with a few complex corner cases. Here are some:

  • did you consider that the first search response can arrive later than the second search response?
  • does your search engine respond too slow and you need caching to improve user experience?
  • do you need to combine your search request from two different requests to zip in some additional data?

Let’s gather some requirements before jumping into code. I’ll go through them one by one in the screencast:

  • search should be incremental, so you don’t need to press the search button to trigger the search task
  • wait 300 ms before executing the search task – the user might be typing, why trigger an unnecessary task – usually a network request
  • if you did send a search request but it hasn’t been completed yet, send a new request and cancel that previous one, so that there is only one running request at a time
  • give the user feedback that there is a network request ongoing
  • give the user feedback when there is no data because of request failure
  • do not reload the table view / collection view, insert and delete rows to make an animated transition
  • cache previous network requests

Step by step guide for building an incremental search in FRP

Watch the screencast all the way or choose the parts you are interested in 🙂

Part 1: Check out the working demo

 

Part 2: Design decisions

Create a list of requirements and map them to design decisions. Before you start coding think how each point corresponds to observables, how they should be mapped or which side effects should be produced. Draw it out on a whiteboard!


 

Part 3: Bare code

A short introduction to the starting point of the screencast.


 

Part 4: Debounce

Debounce is one of many Rx operators. You could also use “`throttle“`. Getting to know the available operators helps a lot, so spend a coffee break with rxmarbles.com 🙂


 

Part 5: FlatMapLatest

“`flatMapLatest“` is another very handy rx operator. We “`flatMap“` the search text stream into network requests. To avoid having 2 simultaneous search tasks running, just take the “Latest” one. The previous one will be canceled. But be careful with error handling! Handle your network errors within the network request observable, or consider materializing that observable.

searchBar
  .rx_text
  .debounce(0.3, scheduler: backgroundScheduler)
  .flatMapLatest(moviesSearchObservable)


 

Part 6: Side Effects

It is important to avoid side effects which usually introduce state. However, performing a network request, giving visual feedback to the user, or bridging to the delegate pattern usually do not give us any other option.

Side effects are a bridge between functional and imperative programming. We can, however, write clean code by injecting the side effects with “`doOn“`, “`doNext“` or “`doError“`

MovieObservables.network(searchText: text).doOn(finishedFetchingMovies)

 


Part 7: Longest Common Subsequence

Probably the most advanced part of this article.

We want to compare two arrays of movies using the LCS algorythm, so we need an observable which streams a tuple of values: <the “`previous“` searched results> and <the “`current“` search results>.

We don’t want to produce additional network requests, so we need to wrap the “`moviesObservable“` into a multicast (shared) observable.

Once we have done that, we can use the multicasted observable to produce two “almost identical streams” which we can zip. The second stream is shifted by one value.

let moviesObservable = searchBar.rx_text
  .debounce(0.3, scheduler: backgroundScheduler)
  .flatMapLatest(moviesSearchObservable)
  .share()

Observable.zip(moviesObservable, moviesObservable.startWith(movies)) { $0 }

 


Part 8: Cache

Don’t reinvent the wheel! Dig into NSURLRequest built in cache policiesOne might match your needs.


 

More to read about FRP

The code: https://github.com/m4c1ek/FRPSearch

FRP guide: https://realm.io/news/frp-ios-guide/

Understand the RX operators: http://rxmarbles.com/

ReactiveCocoa: https://github.com/ReactiveCocoa/ReactiveCocoa

RxSwift: https://github.com/ReactiveX/RxSwift


 

collage1-downMaciej Walczynski is a team leader of the Mobilki Aftonbladet team located in Krakow.

Read here about their work: Crucial to get workflow right when developing mobile app

Go to iTunes and Google Play to get their apps.

 

 

 


 

Written by Maciej Walczyński
Published March 22, 2016