iOS development with Reactive Programming and Model-View-ViewModel (MVVM) - part 2

In the last episode I explained the basic approach of iOS development with Reactive Programming and Model-View-ViewModel (MVVM) architecture. In this part, I will start implementing a simple app using the github API to search for repositories. It will fire requests while the user is typing and the results will get updated reactively. In this article, we will use RxCocoa + RxSwift to show the experience in Reactive MVVM.

 
goal.gif
 

Prerequisites

1. You need a Mac! Download XCode, create an XCode project and call it “RxSwift-Demo”

2. Go to the directory, open the Terminal and type: “command pod init”

3. Open the PodFile and add the following content:

# Podfile
platform :ios, '13.0'
use_frameworks!
target 'Rxswift-Demo' do
pod 'RxSwift', '~> 5'
pod 'RxCocoa', '~> 5'
end

4. In you terminal, type: “pod install”

5. Now, you have RxSwift and RxCocoa in your project.

View/ViewController

In our RxSwift-Demo, we still need to use UIKit and storyboards to create the basic UI elements. We need to have a ViewController (let's call it GithubRepositoriesViewController) that contains two elements, a UITableView and a UISearchBar.

Screenshot 2019-07-11 at 01.02.23.png
class GithubRepositoriesViewController: UIViewController, UITableViewDelegate{
     
    @IBOutlet weak var tableView: UITableView!
    @IBOutlet weak var searchBar: UISearchBar!
   
    let viewModel: GithubRepositoriesViewModel = GithubRepositoriesViewModel()
 
    override func viewDidLoad() {
        super.viewDidLoad()
        self.setupBinding()
    }
    private func setupBinding() {
        //implement the bindings
    }
}

Also, inside the tableview, we need a UITableViewCell to show the repository name and the owner image.

class RepositoryCell: UITableViewCell {
    func setupCell(model: Repository) {
        super.textLabel?.text = model.full_name
        super.imageView?.image = UIImage(named: "placeholder")
  
        if let data = try? Data(contentsOf: model.owner.avatar_url),
             let image = UIImage(data: data) {
             super.imageView?.image = image
        }
    }
}

ViewModel

Next, we will have a ViewModel which handles most of the business logic. Basically, we have only two functions in this class. A search function and a clean up function when there is no query string. The search function is doing the http requests to Github and getting a list of repository infos, storing them in the property repositories.

class GithubRepositoriesViewModel {
    var repositories : [Repository]  = []
 
    func search(query:String) {
            var urlComponents = URLComponents(string: self.baseUrl + "search/repositories")!
             
            var queryItems: [URLQueryItem] {
                return [
                    .init(name: "q", value: query),
                    .init(name: "order", value: "desc")
                ]
            }
            urlComponents.queryItems = queryItems;
            var request = URLRequest(url: urlComponents.url!)
            request.addValue("application/json", forHTTPHeaderField: "Content-Type")
            let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
                guard let data = data else { return }
                 
                DispatchQueue.main.async {
                    do {
                        let decoder = JSONDecoder()
                        let response = try decoder.decode(SearchRepositoryResponse.self, from: data)
                       //This is the result
                        self.repositories = response.items
                    } catch let err {
                        //handle error case
                        print(err)
                    }
                }
            }
            task.resume();
    }
     
    func removeAllRepositories() {
       //remove all the repositories in tableview
       self.repositories = []
    }
}

Models

Now we need some models to represent the data response from github API. In the Github API, for searching a repository, we are expecting a list of Repository. And each of Repository has an owner. In the owner, we can retrieve the avatar image url and display it.

struct SearchRepositoryResponse: Decodable {
    var items: [Repository]
}
 
struct Repository: Decodable, Hashable, Identifiable {
    var id: Int64
    var full_name: String
    var description: String?
    var stargazers_count: Int = 0
    var language: String?
    var owner: User
    var html_url: URL
}
 
struct User: Decodable, Hashable, Identifiable {
    var id: Int64
    var login: String
    var avatar_url: URL
}

Binding Implementation:

Finally our project structure is basically finished. Now we have to implement the binding part:

Firstly, inside GitRepositoriesViewController, we need to add property in this class. We will use the disposeBag to dispose Observables in order to prevent retain cycle, when the ViewController is dismissed. Although this app is a single page app, it is a good practice to have it.

let disposeBag = DisposeBag()

Inside ViewDidLoad, (you can refer to the UIViewController life cycle in here) we should set up the bindings. I create a private function setupBinding to group all the bindings here. There are three main observers in the binding function: observing user typing on the search bar, observing if the repositories are updated in the viewmodel, and observing if user taps on the table cell.

private func setupBinding() {
    searchBar.rx.text.orEmpty.subscribe(onNext: { query in
           self.viewModel.search(query: query)
        }).disposed(by: disposeBag)
     
    viewModel.repositories.asObservable().bind(to: self.tableView.rx.items(cellIdentifier: "RepositoryCell", cellType: RepositoryCell.self)) { index, model, cell in
        cell.setupCell(model: model )
        }.disposed(by: disposeBag)
     
    tableView.rx.modelSelected(Repository.self)
        .subscribe(onNext: { repository in
            UIApplication.shared.open(repository.html_url, options:  [:], completionHandler: nil)
        }).disposed(by: disposeBag)
}

In GithubRepositoriesViewModel, I would like to move all the network logic into a new class GithubServicesClient, so that the GithubRepositoriesViewModel has a cleaner idea to show how to construct the observable for the repositories. Firstly, in order to make the repositories observable, we have to change the property repositories.

let repositories : BehaviorRelay<[Repository]> = BehaviorRelay(value: [])

Inside the GithubServicesClient, we return an observable in each request search and in the GithubRepositoriesViewModel, we will subscribe the returned Observable. The full implementation is shown in here.

//in GithubServicesClient.swift
func search(query: String) -> Observable<[Repository]>  {
        return Observable.create { observer -> Disposable in
             //all the network logic implementation
             if (success) {
                 observer.onNext([repository])
             } else {
                 observer.onError(err)
             }    
    
            return Disposables.create()
        }
}

When we get the repositories from the returned observable, we will pass the value into the BehaviorRelay. And the observer (tableView.rx) from GithubRepositoriesViewController will be notified and update the tableview content.

//in GithubRepositoriesViewModel.swift
let client = GithubServicesClient()
let repositories : BehaviorRelay<[Repository]> = BehaviorRelay(value: [])
  
func search(query:String) {
        guard !query.isEmpty else {
            self.repositories.accept([]);
            return
        }
        client.search(query: query)
            .subscribe(
                onNext: { [weak self] repositories in
                    self?.repositories.accept(repositories)
                },
                onError: { error in
                    print(error)
                }
            )
            .disposed(by: disposeBag)
}

Basically, we are done !! if you run the project you will see this:

 
 

But somehow not right.... Do you see what that is? 

...........

..................

......................... 

You got it!! The images are not loaded asynchronously (not lazy loading) !

Therefore, we need to add push our experiment a bit further. 

We have to give the RepositoryCell a viewModel called RepositoryCellViewModel, so that it can take care of the image download in an asynchronous way. As shown, RepositoryCellViewModel is mapping to one single RepositoryCell. We need one image in each cell and in the cell showing the image whenever it is downloaded. We create a BehaviorRelay property to allow the RepositoryCell to bind with or observe whenever the image is downloaded, this will set the image into the imageView.

class RepositoryCellViewModel {
 
    let image : BehaviorRelay<UIImage?> = BehaviorRelay(value: nil)
 
    func downloadImage(url: URL) {
        URLSession.shared.dataTask( with: url, completionHandler: { (data, _, _) -> Void in
            DispatchQueue.main.async {
                if let data = data {
                    self.image.accept(UIImage(data: data))
                }
            }
        }
        ).resume()
    }
}

Now, we have to update our RepositoryCell. This cell owns a viewModel, and each viewModel will download the image in an asynchronous manner. If the image is loaded, imageView will be notified and updated.

class RepositoryCell: UITableViewCell {
    let viewModel: RepositoryCellViewModel = RepositoryCellViewModel()
    let disposeBag = DisposeBag()
     
    override func awakeFromNib() {
        super.awakeFromNib()
         
        // binding the image in viewModel
        viewModel.image.asObservable().bind(to: super.imageView!.rx.image).disposed(by: self.disposeBag)
         
    }
 
    func setupCell(model: Repository) {
        super.textLabel?.text = model.full_name
        super.imageView?.image = UIImage(named: "placeholder")
        viewModel.downloadImage(url: model.owner.avatar_url)
    }
}

Now if you compile the project and run it, you can get the lazy loading images!

 
goal.gif
 

Conclusion:

It is quite common and easy to implement MVVM patterns with RxSwift and RxCocoa. RxCocoa also helps developer to minimize the UIKit delegates functions implementation. It makes the code much more readable and testable.

You can download the full project in here!

https://github.com/chauchinyiu/Rxswift-Demo

In the next part of this blogpost series I will implement the same features with SwiftUI and Combine to show the difference!