Creating an IOS app with MVVM and RxSwift in minutes

Source: icetime17

Before you go ahead and read the following blog, I want to assert the following:

RxSwift does much of the heavy lifting and makes code quick to write and easy to follow.

and as soon as you complete this example you will believe this too. The implementation in this blog is for intermediate Swift developers with some basic knowledge of RxSwift and MVVM. If you are fairly new, I would recommend going through my other introductory RxSwift blogs:

RxSwift foundation and basic components

Testing in RxSwift and then come back to this one.


MVVM and RxSwift are counted as some of the advanced topics when it comes to IOS app development and many a times I have seen developers getting confused about one or the other. In this blog we will cover both with the help of a working example that will help you to think of these two in much simpler terms and have a high level view of both so that, when need be, you can easily pick either of these to suit your needs.

We can’t do much in an app these days without interacting with a remote server. So in this example we will create an IOS app which interacts with an API, parses data and reflects it on a view. These are usually the some of the main components while writing a basic IOS app these days.

We will create a type ahead search app of GitHub repositories for GitHub user ID and display the results to the user in realtime. Since we are planning to do quite a bit of work, let’s dig in straight away.


Project setup

We will start out in a single view app with a tableView. Clone the projectfrom the repo here, and you will see that I have created a searchController and configured it using configureSearchController() to include the searchBar of the searchController as the tableHeaderView of tableView, as follows:

func configureSearchController() {
searchController.obscuresBackgroundDuringPresentation = false
searchBar.showsCancelButton = true
searchBar.text = "scotteg"
searchBar.placeholder = "Enter GitHub ID, e.g., \"navdeepsinghh\""
tableView.tableHeaderView = searchController.searchBar
definesPresentationContext = true
}

Also, I have already bound the data sequence of a viewModel to the tableView, as shown:

viewModel.data
.drive(tableView.rx.items(cellIdentifier: "Cell")) { _, repository, cell in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.addDisposableTo(disposeBag)

I have created a struct repository with repoName and repoURL string properties, as follows:

struct Repository {

let repoName: String
let repoURL: String
}

Also, I have defined a viewModel class with a searchText variable of string as shown:

class ViewModel {

let searchText = Variable("")

lazy var data: Driver<[Repository]> = {

return Observable.of([Repository]()).asDriver(onErrorJustReturn: [])
}()
}
The purpose of a viewModel is to abstract from the code that prepares the data for use by the viewController, such as binding to the UI. The data property just has a placeholder return so that this project will compile. We will replace it shortly. This data sequence is implemented as a Driver of array of repository — Driver<[Repository]> — and as we see in the viewController, it’ll drive the tableView. Remember that Driver will not emit an error, and it will automatically deliver events on the main thread.

Disclaimer:

The code for completed project is available in the repository: RxSwift_MVVM_Finished, but I would suggest to complete the implementation following this blog and then go through the finished project.


Project implementation

We will start by implementing a repositoriesBy() helper method that will take a Github ID parameter and return an Observable of array of Repository. In a production app, we would put this functionality in an APIManager, but we are intentionally doing this in one file to make it easier to follow. First, we will guard that the GitHub ID parameter is not empty, and then we will successfully create a URL to use our fetch request, as follows:

static func repositoriesBy(_ githubID: String) -> Observable<[Repository]> {
guard !githubID.isEmpty,
let url = URL(string: "https://api.github.com/users/\(githubID)/repos") else {
return Observable.just([])
}
}

If the creation of the URL fails, we will return just an empty array of Observables. You can find GitHub’s documentation for the API.

In order to test out the response from the mentioned URL, you can try it on a browser. For example, my GitHub ID is NavdeepSinghh, and in order to create a URL to fetch my repositories, I use the following URL:

https://api.github.com/users/NavdeepSinghh/repos

Then, if you open this link in the browser, you will see a list of all the repositories under my GitHub ID and some more details, as illustrated:

Here, we can see that we get back an array of dictionaries and the two data points that we are interested in:

Switch back to Xcode, and let’s work on the implementation to pull this data and make full use of it within our application.

Fetching and parsing data

Next, we will get hold of the shared singleton of URLSession and use the rx.json extension on URLSession, passing the URL we created, as shown:

guard !githubID.isEmpty,
let url = URL(string: "https://api.github.com/users/\(githubID)/repos") else {
return Observable.just([])
}

return URLSession.shared.rx.json(url: url)

rx.json returns an Observable sequence of the response JSON. As errors can occur regularly in networking, we will use retry with a max attempt count of 3 so that it retries twice and then, if a third error occurs, we will use catchErrorJustReturn to return an empty array, as follows:

return URLSession.shared.rx.json(url: url)
.retry(3)
.catchErrorJustReturn([])

To read more about error handling in RxSwift, please read my blog on testing in RxSwift.

We don’t need to do this here because the error is handled in the implementation of data because it’s a Driver and Driver cannot emit an error. Here, we just wanted to point out that, if we were not using Driver, we need to see this catch. We will just comment it out to leave it as a reference.

We want to point out that URLSession already returns, so we don’t need to specify a background queue to do the parsing on. If we did want to specify a specific background queue, we would use an observeOn operator here.

Next, we will use map to transform the Observable sequence but, rather than writing the mapping code within the map operator, we will abstract it to a parseJson() method that takes a json and returns an array of repositories. We will start by using a guard to cast the json as an items array of dictionaries of String keys and Any values; if that fails, we will just return an empty array, as follows:

static func parse(json: Any) -> [Repository] {
guard let items = json as? [[String: Any]] else {
return []
}
}

Then, we will create an empty array of repositories to hold each repository instance we create from the json. We will iterate over the items array using the forEach method and in there use a guard to extract the local name and URL String values from the dictionary, as illustrated:

guard let items = json as? [[String: Any]]  else {
return []
}

var repositories = [Repository]()

items.forEach{
guard let repoName = $0["name"] as? String,
let repoURL = $0["html_url"] as? String else {
return
}
}

Then, we will create a new repository instance and append it to the repositories array and finally, we will return the repositories array, as follows:

var repositories = [Repository]()

items.forEach{
guard let repoName = $0["name"] as? String,
let repoURL = $0["html_url"] as? String else {
return
}
repositories.append(Repository(repoName: repoName, repoURL: repoURL))
}
return repositories

Now, back in the repositoriesBy() method, we will pass this method to the map operator.

When we pass the function type directly as a parameter, we just write the name and not the parameter list. This works with functions with a single parameter list and the parameter is inferred to be each value exposed by, in this case, map.

We are not subscribing here; we are just passing through an input to an output, as follows:

static func repositoriesBy(_ githubID: String) -> Observable<[Repository]> {
guard !githubID.isEmpty,
let url = URL(string: "https://api.github.com/users/\(githubID)/repos") else {
return Observable.just([])
}

return URLSession.shared.rx.json(url: url)
.retry(3)
//.catchErrorJustReturn([])
.map(parse)
}

That takes care of fetching and creating the repositories.

Now we will make data use the repositoriesBy() method.

Binding fetched data to View elements

First, we will delete the placeholder return. We want to transform the searchText into an array of repositories. We should point out that the unauthenticated GitHub API we are using has a rate limit of 10 requests per minute.

We will use the throttle operator here to only take the latest element received through the specified time. This will prevent triggering multiple network requests in rapid succession. We will also use the distinctUntilChanged operator, which will allow only unique contiguous elements to pass through.

As the change to text in searchText triggers the operation in this app, this is not necessary here, but we have Search button so that the user can click on the button to explicitly initiate a search. This will help in executing multiple searches in a row on the same string if the user keeps tapping on the search button over and over, as follows:

lazy var data: Driver<[Repository]> = {

return self.searchText.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
}()

Now we will use flatMapLatest. From the previous blogs, you might remember that flatMapLatest switches to the latest Observable sequence and applies a transform to each element. We will pass the static ViewModel.repositoriesBy() method directly to flatMapLatest, which will return an Observable array of repositories each time search results are returned and finally, we will use asDriver(onErrorJustReturn) to convert this to a Driver, just returning an empty array on error, as depicted:

lazy var data: Driver<[Repository]> = {

return self.searchText.asObservable()
.throttle(0.3, scheduler: MainScheduler.instance)
.distinctUntilChanged()
.flatMapLatest(ViewModel.repositoriesBy)
.asDriver(onErrorJustReturn: [])
}()

Now, let’s move over to ViewController; here, we will bind the rx.text of searchBar to the searchText sequence of ViewModel using orEmpty to handle the text value being nil, as shown:

viewModel.data
.drive(tableView.rx.items(cellIdentifier: "Cell")) { _, repository, cell in
cell.textLabel?.text = repository.name
cell.detailTextLabel?.text = repository.url
}
.addDisposableTo(disposeBag)

searchBar.rx.text.orEmpty
.bind(to: viewModel.searchText)
.disposed(by: disposeBag)
As text is entered into searchField, it will put onto the searchText sequence of ViewModel, triggering the network call we set up. This will also handle when the cancel button is tapped on.

We will also use the data sequence of viewModel to drive the title of navigationItem using its rx.title extension, using map to count the number of elements in the data array and embed that value in a string, as follows:

searchBar.rx.text.orEmpty
.bind(to: viewModel.searchText)
.disposed(by: disposeBag)

viewModel.data.asDriver()
.map { "\($0.count) Repositories" }
.drive(navigationItem.rx.title)
.disposed(by: disposeBag)

That’s it!


Build and run

Let’s run the app and try to enter some search text in the Search bar. I will enter my GitHub ID.

You will see the following results:

You will see some repositories either created or contributed to. Delete the text and type in a new GitHub ID to see that person’s repositories as well. You might have noted that the overall process of invoking the API, parsing, and then displaying took a lot less time and comparatively less code.


For other updates you can follow me on Twitter on my twitter handle @NavRudraSambyal

To read more about various other Design patterns and practice examples you can follow the link to my book Reactive programming in Swift 4

Thanks for reading, please share it if you found it useful :)