Sakun Labs 🌲

Binder Architecture with RxSwift

Mar 18, 2018

A quick example of The Binder Architecture with RxSwift.

You would like to fetch a Feed of Podcasts from an API and display it in a view controller.

The model:

struct Podcast: Codable {
    let id: Int
    let title: String
}
  
struct Feed: Codable {
    let podcasts: [Podcast]
}

Here is the fetch part:

enum APIError: Error {
    case serverError
    case parsingError
}
  
final class API {
    func fetchPodcasts() -> Observable<Feed> {
        return Observable.create { observer in
            let url = URL(string: "http://localhost:9000/podcasts-feed")!
            let task = URLSession.shared.dataTask(with: url) {
            	 data, response, error in
                guard let data = data, error == nil else {
                    observer.onError(APIError.serverError)
                    return
                }
                do {
                    let feed = try JSONDecoder().decode(Feed.self, from: data)
                    observer.onNext(feed)
                    observer.onCompleted()
                } catch let error {
                    observer.onError(APIError.parsingError)
                }
            }
            task.resume()
            return Disposables.create {
                task.cancel()
            }
        }
    }
}

fetchPodcasts vends a basic observable. that when subscribed to will send the fetched feed and then complete itself, or it will send an error and then terminate.

But how do you hook this up to a ViewController? Does every controller create its own subscription on viewDidLoad?

We want the view controller to only specify the the views and the business logic (the model, if you will) it is responsible for. It would be nice to keep the binding of the V and the M separate from the controller lifecycle, as much as possible. This gives us the guarantee that the binding is deterministic, declarative, and will not being changed with the controller's lifecycle.

class ViewController: UIViewController {
    // specifying the "model"
    let feed = Variable<Feed>(Feed(podcasts: []))
    // specifying the view
    @IBOutlet weak var tableView: UITableView!
    
    // specifying some binding helpers
	let disposeBag = DisposeBag()
    let didAppear = Variable<Void>(())

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // let the service know we are ready
        didAppear.value = ()
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // set up our view
        tableView.dataSource = self
    }
}

// Work with UIKit. This could be abstracted out to a "service" too.
extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return feed.value.podcasts.count
    }
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
        let podcast = feed.value.podcasts[indexPath.item]
        cell.textLabel?.text = podcast.title
        return cell
    }
}

Then, we may have a relevant service for this part of our app:

class Service {
    let api = API()
}

To actually do the binding, we can keep it anywhere, but might as well in a static function of the controller:

extension ViewController {
    class func make(service: Service) -> UIViewController {
        let controller = ViewController()
        controller.didAppear
            .asObservable()
            .bind {
                let fetch = service.api.fetchPodcasts()
                    .share(replay: 1, scope: .forever)

                fetch.bind(to: controller.feed)
                    .disposed(by: controller.disposeBag)

                fetch.asObservable()
                    .observeOn(MainScheduler.instance)
                    .subscribe(onNext: { _ in controller.tableView.reloadData() })
                    .disposed(by: controller.disposeBag)
            }.disposed(by: controller.disposeBag)
        return controller
    }
}

Then in the AppDelegate:

let window = UIWindow()
window.rootViewController = ViewController.make(service: Service())
self.window = window
window.makeKeyAndVisible()

I like this architecture a lot. It does rely heavily on FRP, so may not be a great fit for all codebases. But you can happily apply it in small parts to see how it works for you.