Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

25. Building a Complete RxSwift App
Written by Florent Pillet

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

Throughout this book, you’ve learned about the many facets of RxSwift. Reactive programming is a deep topic; its adoption often leads to architectures very different from the ones you’ve grown used to. The way you model events and data flow in RxSwift is crucial for proper behavior of your apps, as well as protecting against issues in future iterations of the product.

To conclude this book, you’ll architect and code a small RxSwift application. The goal is not to use Rx “at all costs”, but rather to make design decisions that lead to a clean architecture with stable, predictable and modular behavior. The application is simple by design, to clearly present ideas you can use to architect your own applications.

This chapter is as much about RxSwift as it is about the importance of a well-chosen architecture that suits your needs. RxSwift is a great tool that helps your application run like a well-tuned engine, but it doesn’t spare you from thinking about and designing your application architecture.

Regardless of the merits and strengths of each architecture, remember that the best architecture is the one that suits your needs. In this chapter you’ll explore ideas for an MVVM-based architecture, but make sure you look around other architectural patterns to pick the best for the task at hand.

Introducing QuickTodo

Serving as the modern equivalent of the “hello world” program, a “To-Do” application is an ideal candidate to expose the inner structure of an Rx application.

In the previous chapter, you learned about MVVM and how well it fits with reactive programming.

You’ll structure the QuickTodo application with MVVM and learn how you can isolate the data-processing parts of your code and make them fully independent.

Architecting the application

One particularly important goal of your app is to achieve a clean separation between the user interface, the business logic of your application, and the internal “services” the app contains to help the business logic run. To that end, you really need a clean model where each component is clearly identified.

Bindable view controllers

You’ll start with the view controllers. At some point, you need to connect, or bind, the view controllers to their associated view model. One way to do this is to have your controllers adopt a specific protocol: BindableType.

protocol BindableType: AnyObject {
  associatedtype ViewModelType

  var viewModel: ViewModelType! { get set }

  func bindViewModel()
}

Binding at the right time

There’s one particular aspect of binding you need to be careful about. You want the viewModel property to be assigned to your view controller as soon as possible, but bindViewModel() must be invoked only after the view has been loaded.

extension BindableType where Self: UIViewController {
  func bindViewModel(to model: Self.ViewModelType) {
    viewModel = model
    loadViewIfNeeded()
    bindViewModel()
  }
}

Task model

Your task model is simple and derives from the Realm base object. A task is defined as having a title (the task contents), a creation date and a checked date. Dates are used to sort tasks in the tasks list.

class TaskItem: Object {
	@objc dynamic var uid: Int = 0
	@objc dynamic var title: String = ""

	@objc dynamic var added: Date = Date()
	@objc dynamic var checked: Date? = nil

	override class func primaryKey() -> String? {
		return "uid"
	}
}

Tasks service

The tasks service is responsible for creating, updating and fetching task items from the store. As a responsible developer, you’ll define your service’s public interface using a protocol and then write the runtime implementation and a mock implementation for tests.

protocol TaskServiceType {
  @discardableResult
  func createTask(title: String) -> Observable<TaskItem>

  @discardableResult
  func delete(task: TaskItem) -> Observable<Void>

  @discardableResult
  func update(task: TaskItem, title: String) -> Observable<TaskItem>

  @discardableResult
  func toggle(task: TaskItem) -> Observable<TaskItem>

  func tasks() -> Observable<Results<TaskItem>>
}
@discardableResult
func update(task: TaskItem, title: String) -> Observable<TaskItem> {
  let result = withRealm("updating title") { realm -> Observable<TaskItem> in
    try realm.write {
      task.title = title
    }
    return .just(task)
  }
  return result ?? .error(TaskServiceError.updateFailed(task))
}

Scenes

You learned earlier that a scene is a logical presentation unit made of a view managed by a view controller and a view model. The rules for scenes are:

enum Scene {
  case tasks(TasksViewModel)
  case editTask(EditTaskViewModel)
}
extension Scene {
  func viewController() -> UIViewController {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    switch self {
    case .tasks(let viewModel):
      let nc = storyboard.instantiateViewController(withIdentifier: "Tasks") as! UINavigationController
      let vc = nc.viewControllers.first as! TasksViewController
      vc.bindViewModel(to: viewModel)
      return nc

    case .editTask(let viewModel):
      let nc = storyboard.instantiateViewController(withIdentifier: "EditTask") as! UINavigationController
      let vc = nc.viewControllers.first as! EditTaskViewController
      vc.bindViewModel(to: viewModel)
      return nc
    }
  }
}

Coordinating scenes

One of the most puzzling questions when developing an architecture around MVVM is, “How does the application transition from scene to scene?”. There are many answers to this question, as every architecture has a different take on it. Some do it from the view controller, because of the need to instantiate another view controller; while some do it using a router, which is a special object thats connects view models.

Transitioning to another scene

You will use a simple solution which has proved its effectiveness over many applications:

The scene coordinator

The scene coordinator is defined through a SceneCoordinatorType protocol. A concrete SceneCoordinator implementation is provided to run the application. You can also develop a test implementation that fakes transitions.

protocol SceneCoordinatorType {
  @discardableResult
  func transition(to scene: Scene, type: SceneTransitionType) -> Completable

  @discardableResult
  func pop(animated: Bool) -> Completable
}
_ = navigationController.rx.delegate
  .sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
  .map { _ in }
  .bind(to: subject)
return subject.asObservable()
  .take(1)
  .ignoreElements()

Passing data back

Passing data back from a scene to the previous one, such as the result of a modal dialog, is simple with RxSwift. A presenting view model instantiates the view model for the presented scene, so it can access it and can set up communication. Any of the following techniques will be useful:

Kicking off the first scene

The final detail about using a coordinated scene model is the startup phase; you need to kick off the scene’s presentation by introducing the first scene. This is a process you’ll perform in your application delegate.

let service = TaskService()
let sceneCoordinator = SceneCoordinator(window: window!)
let tasksViewModel = TasksViewModel(taskService: service, coordinator: sceneCoordinator)
let firstScene = Scene.tasks(tasksViewModel)
sceneCoordinator.transition(to: firstScene, type: .root)

Binding the tasks list with RxDataSources

In Chapter 18, “Table and Collection Views”, you learned about the UITableView and UICollectionView reactive extensions built in RxCocoa. In this chapter, you’ll learn how to use RxDataSources, a framework available from the RxSwiftCommunity GitHub organization and originally developed by Krunoslav Zaher, the creator of RxSwift.

typealias TaskSection = AnimatableSectionModel<String, TaskItem>
extension TaskItem: IdentifiableType {
  var identity: Int {
    return self.isInvalidated ? 0 : uid
  }
}
var sectionedItems: Observable<[TaskSection]> {
  return self.taskService.tasks()
    .map { results in
      let dueTasks = results
        .filter("checked == nil")
        .sorted(byKeyPath: "added", ascending: false)

      let doneTasks = results
        .filter("checked != nil")
        .sorted(byKeyPath: "checked", ascending: false)

      return [
        TaskSection(model: "Due Tasks", items: dueTasks.toArray()),
        TaskSection(model: "Done Tasks", items: doneTasks.toArray())
      ]
    }
}
var dataSource: RxTableViewSectionedAnimatedDataSource<TaskSection>!
private func configureDataSource() {
  dataSource = RxTableViewSectionedAnimatedDataSource<TaskSection>(
    configureCell: {
      [weak self] dataSource, tableView, indexPath, item in
      let cell = tableView.dequeueReusableCell(withIdentifier: "TaskItemCell", for: indexPath) as! TaskItemTableViewCell
      if let self = self {
        cell.configure(with: item, action: self.viewModel.onToggle(task: item))
      }
      return cell
    },
    titleForHeaderInSection: { dataSource, index in
      dataSource.sectionModels[index].model
    })
}

configureDataSource()
viewModel.sectionedItems
  .bind(to: tableView.rx.items(dataSource: dataSource))
  .disposed(by: self.rx.disposeBag)

Binding the Task cell

You’ll apply this technique to TaskItemTableViewCell. Open the class file and add some meat to the configure(with:action:) method:

button.rx.action = action
item.rx.observe(String.self, "title")
  .subscribe(onNext: { [weak self] title in
    self?.title.text = title
  })
  .disposed(by: disposeBag)

item.rx.observe(Date.self, "checked")
  .subscribe(onNext: { [weak self] date in
    let image = UIImage(named: date == nil ? "ItemNotChecked" : "ItemChecked")
    self?.button.setImage(image, for: .normal)
  })
  .disposed(by: disposeBag)
override func prepareForReuse() {
  button.rx.action = nil
  disposeBag = DisposeBag()
  super.prepareForReuse()
}

Editing tasks

The next problem to tackle is the creation and modification of tasks. You‘ll want to present a modal view controller when creating or editing a task, and actions (such as updating or deleting) should propagate back to the tasks list view model.

func onCreateTask() -> CocoaAction {
  return CocoaAction { _ in
    return self.taskService
      .createTask(title: "")
      .flatMap { task -> Observable<Void> in
        let editViewModel = EditTaskViewModel(task: task,
          coordinator: self.sceneCoordinator,
          updateAction: self.onUpdateTitle(task: task),
          cancelAction: self.onDelete(task: task))
        return self.sceneCoordinator
          .transition(to: Scene.editTask(editViewModel), type: .modal)
          .asObservable()
          .map { _ in }
      }
  }
}
newTaskButton.rx.action = viewModel.onCreateTask()
onUpdate.executionObservables
  .take(1)
  .subscribe(onNext: { _ in
    coordinator.pop()
  })
  .disposed(by: disposeBag)
onCancel = CocoaAction {
  if let cancelAction = cancelAction {
    cancelAction.execute(())
  }
  return coordinator.pop()
    .asObservable()
    .map { _ in }
}
cancelButton.rx.action = viewModel.onCancel

okButton.rx.tap
  .withLatestFrom(titleView.rx.text.orEmpty)
  .bind(to: viewModel.onUpdate.inputs)
  .disposed(by: self.rx.disposeBag)
lazy var editAction: Action<TaskItem, Swift.Never> = { this in
  return Action { task in
    let editViewModel = EditTaskViewModel(
      task: task,
      coordinator: this.sceneCoordinator,
      updateAction: this.onUpdateTitle(task: task)
    )
    return this.sceneCoordinator
      .transition(to: Scene.editTask(editViewModel), type: .modal)
      .asObservable()
  }
}(self)
tableView.rx.itemSelected
  .map { [unowned self] indexPath in
    try! self.dataSource.model(at: indexPath) as! TaskItem
  }
  .bind(to: viewModel.editAction.inputs)
  .disposed(by: self.rx.disposeBag)
  .do(onNext: { [unowned self] indexPath in
    self.tableView.deselectRow(at: indexPath, animated: false)
  })

Challenges

Challenge 1: Support item deletion

You’ve probably noticed that it isn’t possible to delete items. You’ll need to make changes to both TaskViewModel and TaskViewController to add this functionality. For this challenge, start from the final project of this chapter. Once you complete the challenge, the users will be able to swipe on a task and delete it:

setEditing(true, animated: false)

Challenge 2: Add live statistics

To make the UI more interesting, you want to display the number of due and done items in your list. A label is reserved for this purpose at the bottom of the TasksViewController view; it’s connected to statisticsLabel. For this challenge, start from either your solution to the previous challenge, or from the chapter’s final project.

typealias TaskStatistics = (todo: Int, done: Int)

Challenge 3: Support a Back button in navigation

A frequently-asked question about the Coordinator pattern is: “How do I support the Back button in navigation?” One of the issues with navigation is that the Back button is directly handled by UINavigationController, and thus largely invisible to SceneCoordinator.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now