25.
Building a Complete RxSwift App
Written by Florent Pillet
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.
First, let’s introduce some terminology for the architecture you‘re going to implement:
- Scene: Refers to a view managed by a view controller. It can be a regular view, or a modal dialog. It comprises a view controller and a view model.
- View model: Defines the business logic and data used by the view controller to present a particular scene.
- Service: A logical group of functionality provided to any scene in the application. For example, storage to a database can be abstracted to a service. Likewise, requests to a network API can be grouped in a network service.
- Model: The most basic data store in the application. View models and services both manipulate and exchange models.
You learned about View Models in the previous chapter, “MVVM with RxSwift.”
Services are a new concept and another good fit for reactive programming. Their purpose is to expose data and functionality using Observable and Observer as much as possible, so as to create a global model where components connect together as reactively as possible.
For your QuickTodo application, the requirements are relatively modest.
You’ll architect it correctly nonetheless, so you have a solid foundation for future growth.
It’s also an architecture you’ll be able to reuse in other applications.
The basic items you need are:
- A
TaskItemmodel that describes an individual task. - A
TaskServiceservice that provides task creation, updating, deletion, storage and search capabilities. - A storage medium; you’ll use a database (Realm) and RxRealm, another project living under the RxSwiftCommunity organization https://git.io/JJXNh, which helps you seamlessly integrate Realm into your reactive workflow..
- A series of scenes to list, create and search tasks. Each scene is split into a view model and a view controller.
- A scene coordinator object to manage scene navigation and presentation.
Here is how the basic items relate to each other:
As you learned in the previous chapter, the view model exposes the business logic and the model data to the view controller. The rules you’ll follow to create the ViewModel for each scene are simple:
- Expose data as
Observablesequences. This guarantees automatic updates once connected to the user interface. - Expose all ViewModel actions connectable to the UI using the
Actionpattern you learned about in Chapter 20. - Any model or data publicly accessible and not exposed as an observable sequence is immutable.
- Transitioning from scene to scene is part of the business logic. Each View Model initiates this transition and prepares the next scene’s view model, but doesn’t know anything about the view controller.
A solution to fully insulate View Models from the actual ViewController, including triggering transitions to other scenes, is laid out later in this chapter.
Note: Data immutability guarantees total control over updates triggered by the UI. Strict observance of the rules above also guarantees the best testability of each part of the code.
The previous chapter showed how to use a mutable property to update the underlying model with the help of
didSet. This chapter will take the notion further by completely removing mutability and only exposingActions.
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.
Note: The starter project for this chapter includes quite some code. When you first open the project in Xcode it will not compile successfully, as you need to add some required types before you can build and run for the first time.
Open BindableType.swift and add the basic protocol:
protocol BindableType: AnyObject {
associatedtype ViewModelType
var viewModel: ViewModelType! { get set }
func bindViewModel()
}
Each view controller conforming to the BindableType protocol will declare a viewModel property and provide a bindViewModel() method to be called once the viewModel property is assigned. This method will connect UI elements to observables and actions in the view model.
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.
The reason is that your bindViewModel() will typically connect UI elements that need to be present. Therefore, you’ll use a small helper method to call it after instantiating each view controller. Add this to BindableType.swift:
extension BindableType where Self: UIViewController {
func bindViewModel(to model: Self.ViewModelType) {
viewModel = model
loadViewIfNeeded()
bindViewModel()
}
}
This way, by the time viewDidLoad() is called in your view controller, you’re sure the viewModel property has already been assigned.
Since viewDidLoad() is the best time to set your view controller’s title for a smooth push navigation title animation, and knowing you might require access to your view model to prepare the title, loading the view controller only when required is what works best for all cases.
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.
If you’re not familiar with Realm, check out their documentation at https://realm.io/docs/swift/latest/.
Populate TaskItem.swift as follows:
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"
}
}
There are three details you need to be aware of that are specific to objects coming from a Realm database:
- Objects can’t cross thread boundaries. If you need an object in a different thread, either re-query it or use Realm‘s
ThreadSafeReference. - Objects are auto-updating. If you make a change to the database, it‘d be immediately reflected in the properties of any live objects queried from the database. This has its uses as you’ll see further down.
- As a consequence, deleting an object invalidates all existing copies. If you access any property of a queried object that is deleted, you’ll get an exception.
The second point above has side effects, which you’ll study in greater detail later in this chapter when binding the task cell.
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.
First, create the protocol. This is what you’ll expose to the consumers of the service. Open TaskServiceType.swift and fill in the protocol definition:
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>>
}
This is a basic interface providing the fundamental services to create, delete, update and query tasks. Nothing fancy here. The most important detail is that the service exposes all data as observable sequences. Even the methods which create, delete, update and toggle tasks return an observable you can subscribe to.
The core idea is to convey any failures or successes of the operation through successful completion of the observables. In addition, you can use the returned observable as the return value in Actions. You’ll see some examples of this later in the chapter. For example, open TaskService.swift and you’ll see update(task:title:) looks like this:
@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))
}
withRealm(_:action:) is an internal helper function which gets the current Realm database and starts an operation on it. In case an error is thrown, withRealm(_:action:) will always return nil. This is a good occasion to return an error observable to signal the error to the caller. You won’t go through the complete implementation of the tasks service, but you can read through the code in TaskService.swift.
You’re done with the tasks service! Your view models will receive a TaskServiceType object, either real or mocked (for tests), and will be able to perform their work.
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:
- The view model handles the business logic. This extends to kicking off the transition to another scene.
- View models know nothing about the actual view controller and views used to represent the scene.
- View controllers shouldn’t initiate the transition to another scene; this is the domain of the business logic running in the view model.
With this in mind you can lay down a model where application scenes are listed as cases in a Scene enumeration, and each case has the scene view model as its associated value.
Note: This is similar to what you did in the previous chapter in the
Navigatorclass, but here, the navigation is even more flexible by using scenes.
Open Scene.swift. You’ll define the two scenes you’ll need in this simple app, tasks and editTask. Add:
enum Scene {
case tasks(TasksViewModel)
case editTask(EditTaskViewModel)
}
At this stage, a view model can instantiate another view model and assign it to its scene, ready for transition. You also fulfill the basic contract for view models, which, as much as possible, shouldn’t depend on UIKit at all.
An extension to the Scene enum that you’ll add in a moment exposes a method which is the only place you’ll instantiate a view controller for a scene. This method will know how to pull the view controller from its resources for each scene.
Open Scene+ViewController.swift and add the following method:
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
}
}
}
The code instantiates the appropriate view controller and immediately binds it to its view model, which is taken from the data associated to each enum case.
Note: This method can become quite long when you have many scenes in your application. Don’t hesitate to split it up into multiple sections for clarity and ease of maintenance. In a large application with multiple domains, you could even have a primary
enumdefining domains, and sub-enums with the scenes for each domain.
Finally a scene coordinator handles the transition between scenes. Each view model knows about the coordinator and can ask it to push a scene.
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:
- A view model creates the view model for the next scene.
- The first view model initiates the transition to the next scene by calling into the scene coordinator.
- The scene coordinator uses an extension method to the
Scenesenum to instantiate the view controller. - Next, the scene controller binds the controller to the next view model.
- Finally, it presents the next scene view controller.
With this structure, you can completely isolate view models from the view controllers using them, and also insulate them from the details of where to find the next view controller to push. Later in this chapter, you’ll see how to use the Action pattern to wrap steps 1 and 2 above and kick off a transition.
Note: It’s important that you always call the scene coordinator’s
transition(to:type:)andpop()methods to transition between scenes, as the coordinator needs to keep track of which view controller is frontmost, particularly when presenting scenes modally. Do not use automatic segues. Any automatic transition happening (like usage of a “back” button directly managed by the navigation controller) should be intercepted, for example via delegate methods, to let the scene coordinator know about the change.
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.
The SceneCoordinatorType protocol already provided in the starter project is simple, yet efficient:
protocol SceneCoordinatorType {
@discardableResult
func transition(to scene: Scene, type: SceneTransitionType) -> Completable
@discardableResult
func pop(animated: Bool) -> Completable
}
The two methods transition(to:type:) and pop(animated:) let you perform all the transitions you need: push, pop, modal, and dismiss.
The concrete implementation in SceneCoordinator.swift shows some interesting cases of intercepting delegate messages with RxSwift. Both transition calls were designed to return a Completable that completes once the transition is complete. You can subscribe to it to take further action, as it works like a completion callback.
To implement this, the code included in the project creates a UINavigationController DelegateProxy, an RxSwift proxy which can intercept messages while forwarding messages to the actual delegate:
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.map { _ in }
.bind(to: subject)
The trick, found at the bottom of the transition(to:type:) method, is to bind this subscription to a subject returned to the caller:
return subject.asObservable()
.take(1)
.ignoreElements()
The returned observable will take at most one emitted element to handle the navigation case, but doesn’t forward it, and completes.
Note: You may question the memory safety of this construct because of the unbounded subscription to the navigation delegate proxy. It’s totally safe: the returned observable will take at most one element, then completes. When completing, it disposes of its subscriptions. If nothing subscribes to the returned observable, the subject is disposed from memory and its subscriptions terminate as well.
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:
-
The presenting view model can pass a traditional closure to the presented view model, which can later call it to produce a result.
-
Expose an
Observablein the presented view model, that the presenting view model can subscribe to. When the second view model dismisses the presentation, it can emit one or more result elements on the observable. -
Pass an
Observerobject, such as aRelayor aSubject, to the presented view model, which will use that object to emit results. -
Pass one or more
Actions to the presented view model, to be executed with the appropriate result.
These techniques allow for excellent testability and help you avoid playing games with weak references between models. You’ll see an example of this later in this chapter when adding the Edit Task view controller.
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.
Open AppDelegate.swift and add the following code to the beginning of application(_:didFinishLaunchingWithOptions:):
let service = TaskService()
let sceneCoordinator = SceneCoordinator(window: window!)
The first step is to prepare all the services you need along with the coordinator. Then, instantiate the first view model and instruct the coordinator to set it as the root.
let tasksViewModel = TasksViewModel(taskService: service, coordinator: sceneCoordinator)
let firstScene = Scene.tasks(tasksViewModel)
sceneCoordinator.transition(to: firstScene, type: .root)
That was easy! The cool thing with this technique is that you can use a different startup scene if needed; for example, a tutorial that runs the first time the user opens your application.
Now that you’ve completed the setup for your initial scene, you can take a look at your individual view controllers.
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.
The reason this framework isn’t part of RxCocoa is mainly that it is deeper and more complex than the simple extensions RxCocoa provides. It comes with the following benefits:
- Support for sectioned tables and collection views.
- Optimized (partial) reloads for deletions, insertions and updates, thanks to an efficient differentiation algorithm.
- Configurable animations for deletions, insertions and updates.
- Support for both section and item animations.
In your case, adopting RxDataSources will give you automatic animations without doing any work. The goal is to move checked items at the end of the tasks list into a “checked” section.
The downside of RxDataSources is that it is initially more difficult to understand than the basic RxCocoa bindings. Instead of passing an array of items to the table or collection view, you pass an array of section models.
The section model defines both what goes in the section header (if any), and the data model of each item of that section.
The simplest way to start using RxDataSources is to use the already provided SectionModel or AnimatableSectionModel generic types as the type for your section. Since you want to animate items, you’ll go for AnimatableSectionModel. You can use the generic class as is by simply specifying the types of the section information and the items array.
Open TasksViewModel.swift and add this to the top, before your struct:
typealias TaskSection = AnimatableSectionModel<String, TaskItem>
This defines your section type as having a section model of type String, since you just need a title, and section contents as an array of TaskItems.
The only constraint with RxDataSources is that each type used in a section must conform to the IdentifiableType and Equatable protocols. IdentifiableType declares a unique identifier unique among objects of the same concrete type, so that RxDataSources can uniquely identify data models. Equatable lets it compare objects to detect changes between two copies of the same unique object.
Realm objects already conform to the Equatable protocol (see note below for a few gotchas). Now you simply need to declare TaskItem as conforming to IdentifiableType. Open TaskItem.swift and add the following extension at its end:
extension TaskItem: IdentifiableType {
var identity: Int {
return self.isInvalidated ? 0 : uid
}
}
This code checks for object invalidation by the Realm database. This happens when you delete a task; any live copy previously queried from the database becomes invalid.
Note: Change detection is a little challenging in your case because Realm objects are a class type, not a value type. Any update to the database immediately reflects in the object properties, which makes comparison difficult for RxDataSources. In fact, Realm’s implementation of the
Equatableprotocol is fast because it only checks whether two objects refer to the same stored object. See the “Task cell” section below for a solution to this specific issue.
Now you need to expose your tasks list as an observable. You’ll be using your TaskService’s tasks observable which, thanks to RxRealm, automatically emits when a change occurs in the tasks list. Your goal is to split the tasks list like so:
- Due (unchecked) tasks first, sorted by last-added-first.
- Done (checked) tasks, sorted by checked data (last checked first).
Add the following code to your TasksViewModel struct:
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())
]
}
}
By returning an array with two TaskSection elements, you automatically create a list with two sections.
Now, on to the TasksViewController. Some interesting work will happen here to bind the sectionedItem observable to the table view. The first step is to create a data source suitable for use with RxDataSources.
For table views, it can be one of two:
-
RxTableViewSectionedReloadDataSource<SectionType>. -
RxTableViewSectionedAnimatedDataSource<SectionType>.
The Reload type isn’t very advanced. When the section observable it subscribes to emits a new list of sections, it simply reloads the table by using reloadData() internally.
The Animated type is the one you want. Not only does it perform partial reloads, but it also animates every change. Add the following dataSource property to the TasksViewController class:
var dataSource: RxTableViewSectionedAnimatedDataSource<TaskSection>!
The major difference with RxCocoa’s built-in table view support is that you set up the data source object to display each cell type, instead of doing it in the subscription.
Within the tasks view controller, add a method to create and “skin” the datasource:
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
})
}
As you learned in Chapter 18, “Table and Collection Views,” when binding an observable to a table or collection view, you provide a closure to produce and configure each cell as needed. RxDataSources works the same way, but the configuration is all performed in the “data source” object.
There’s one detail about this configuration code that’s key to this MVVM architecture. Notice how you passed an Action to the configuration method?
This is the way your design handles actions triggered from cells, that propagate back to the view model.
It’s much like a closure, except the action is provided by the view model, and the view controller limits its role to connecting the cell with the action.
In the end, it works like this:
The interesting part is that the cell itself, aside from assigning the action to its button (see below), doesn’t have to know anything about the view model itself.
Note: The
titleForHeaderInSectionclosure returns a string title for section headers. This is the simplest case for creating section headers. If you want something more elaborate, you can configure it by settingdataSource.supplementaryViewFactoryto return an appropriateUICollectionReusableViewfor theUICollectionElementKindSectionHeaderkind.
Since viewDidLoad() is the place where the table view is placed in auto-height mode, that’s a good place to complete the table configuration. The only requirement of RxDataSources is that the data source configuration must be done before you bind an observable.
In viewDidLoad() add:
configureDataSource()
Finally, bind the view model’s sectionedItems observable to the table view via its data source in the bindViewModel() method:
viewModel.sectionedItems
.bind(to: tableView.rx.items(dataSource: dataSource))
.disposed(by: self.rx.disposeBag)
You’re done with the first controller! You can use different animations for each change type in your dataSource object. Leave them at the default for now.
The cell used to display an item in the Tasks list is an interesting case. In addition to using the Action pattern to relay the “checkmark toggled” information back to the view model (see figure above), it has to deal with the fact that the underlying object, a Realm Object instance, may change during display.
Fortunately, RxSwift has a solution to this problem. Since objects stored in a Realm database use dynamic properties, they can be observed with KVO. With RxSwift you can use object.rx.observe(class, propertyName) to create an observable sequence from changes to the property!
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
You first bind the “toggle checkmark” action to the checkmark button. Check out Chapter 20, “Action,” for more details on the Action pattern.
Next, bind the title string and “checked” status image:
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)
Here you individually observe both properties and update the cell contents accordingly. Since you immediately receive the initial value at subscription time, you can be confident that the cell is always up to date.
Finally, don’t forget to dispose your subscriptions. Failing to do so would lead to some nasty surprises when the cell is reused by the table view!
Add the following:
override func prepareForReuse() {
button.rx.action = nil
disposeBag = DisposeBag()
super.prepareForReuse()
}
This is the correct way to clean things up and prepare for cell reuse. Always be very careful not to leave dangling subscriptions! In the case of a cell, since the cell itself is reused, it’s essential that you take care of this.
Build and run the application. You should be able to see a default list of tasks. Check one off, and the nice animation you see is automatically generated by RxDataSources’ differentiator algorithm!
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.
While not absolutely necessary in this case, as changes could be handled locally and the tasks list will update automatically, thanks to Realm, it is important that you learn patterns for passing information back in a sequence of scenes.
One way to achieve this is to use the trusted Action pattern. Here’s the plan:
- When preparing the edit scene, pass it one or more actions at initialization time.
- The edit scene performs its work and executes the appropriate action (update or cancel) on exit.
- The caller can pass different actions depending on its context, and the edit scene won’t know the difference. Pass a “delete” action for canceling at creation time, or an empty action (e.g. no action) for canceling an edit.
You’ll find this pattern to be quite flexible when you apply it to your own applications. It is particularly useful when presenting modal scenes, but also to convey the result of one of more scenes for which you want a synthetic result set passed.
Time to put this into practice. Add the following method to TasksViewModel:
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 }
}
}
}
Note: Since
selfis astruct, the action gets its own “copy” of the struct (hopefully optimized by Swift to being just a reference), and there is no circular reference - no risk of leaking memory! That’s why you don’t see[weak self]or[unowned self]here, which don’t apply to value types.
This is the action you’ll bind to the “+” button at the top-right of the tasks list scene. Here’s what it does:
- Creates a fresh, new task item.
- If creation is successful, instantiates a new
EditTaskViewModel, passing it anupdateAction, which updates the title of the new task item, and acancelActionwhich deletes the task item. Since it was just created, canceling should logically delete the task. - Since
transition(to:type:)returns aCompletableandCocoaActionexpects anObservable<Void>(this may change in the future), the last line in thereturnstatement performs the required conversion to an Observable sequence ofVoid.
Note: Since an
Actionreturns an observable sequence, you integrate the whole create-edit process into a single sequence that completes once the Edit Task scene closes. Since anActionstays locked until the execution observable completes, it is not possible to inadvertently raise the editor twice at the same time. Cool!
Now, time to bind the action to the “+” button on the bindViewModel() method of TasksViewController:
newTaskButton.rx.action = viewModel.onCreateTask()
Next, move to EditTaskViewModel.swift and populate the initializer. Add this code to init(task:coordinator:updateAction:cancelAction:):
onUpdate.executionObservables
.take(1)
.subscribe(onNext: { _ in
coordinator.pop()
})
.disposed(by: disposeBag)
What does the above do? Besides setting the onUpdate action to be the action passed to the initializer, it subscribes to the action’s executionObservables sequence which emits a new observable when the action executes. Since the action will be bound to the OK button, you‘ll only see it executed once. When that happens, you pop() the current scene, and the scene coordinator dismisses it.
For the Cancel button, you need to proceed differently. Remove the existing onCancel = cancelAction assignment; you’ll do something a little more clever.
Since the action received by the initializer is optional, as the caller may not have anything to do on cancel, you need to generate a new Action. Therefore, this will be the occasion to pop() the scene:
onCancel = CocoaAction {
if let cancelAction = cancelAction {
cancelAction.execute(())
}
return coordinator.pop()
.asObservable()
.map { _ in }
}
Note: To allow most of the code to compile, the
onUpdateandonCancelproperties were defined as forced-unwrapped optionals. You can remove the exclamation marks now.
Finally, move to the EditTaskViewController (in EditTaskViewController.swift) class to finalize the UI binding. Add this to bindViewModel():
cancelButton.rx.action = viewModel.onCancel
okButton.rx.tap
.withLatestFrom(titleView.rx.text.orEmpty)
.bind(to: viewModel.onUpdate.inputs)
.disposed(by: self.rx.disposeBag)
All you have to do to handle the UI is pass the text view contents to the onUpdate action when the user taps the OK button. You’re taking advantage of Action’s inputs observer which lets you pipe values directly for execution of the action.
Build and run the application. Create new items and update their titles to see everything in action.
The last thing to tackle is the addition of existing items. For this, you’ll need a new Action that isn’t temporary; remember that actions have to be referenced other than via a subscription, otherwise they’ll be deallocated. As mentioned in Chapter 20, this is a frequent source of confusion.
Create a new lazy variable in TasksViewModel:
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)
Did you notice the Swift.Never type for the returned sequence? Since transition(to:type:) returns a Completable sequence which, when turned to an observable sequence, translates to Observable<Swift.Never>, to indicate that no element is ever emitted, you also convey this information in the sequence type returned by the action.
Note: Since
selfis astructyou can’t createweakorunownedreferences. Instead, passselfto the closure or function that initialized the lazy variable.
Now, back in TasksViewController.swift, you can bind this action in TaskViewController’s bindViewModel(). Add:
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)
You’re using dataSource to obtain the model object matching the received IndexPath, then piping it into the action’s inputs. Sweet!
One final nitpicking point: you’ll notice that after tapping a row to edit an item, if you press the Cancel button, the row will stay selected. It’s an easy fix with the do(onNext:) operator. Insert this code between the tableView.rx.itemSelected line and the map(_:) operator:
.do(onNext: { [unowned self] indexPath in
self.tableView.deselectRow(at: indexPath, animated: false)
})
Build and run the application: you can now create and edit tasks! Hooray!
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:
The easiest way to get started is to put the controller in edit mode all the time. This will activate support for swiping right-to-left on cells so that you can reveal the Delete button. In viewDidLoad, you can turn that feature on this way:
setEditing(true, animated: false)
The second change will be in your dataSource object. You need to indicate that all the cells can be “edited”. Dig through RxDataSources’ TableViewSectionedDataSource class and I’m sure you’ll find what you need to set. Hint: It’s a closure, and you can simply return true in all cases.
Now you can get to the core of the challenge: handling the actual deletion. The solution to this challenge involves:
- Creating an
ActioninTasksViewModelsuch that, given a model item, will call the appropriate API inTaskService. Can you figure out its signature? If not, read on! - In
TasksViewController, bind this action totableView.rx.itemDeleted. You’ll have to figure out how to go from theIndexPathyou receive to aTaskItem.
You won’t reuse the existing onDelete(task:) function because it returns a CocoaAction, not an Action<TaskItem,Void>.
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.
Gathering live statistics involves the following:
- Adding a single new API to
TaskServiceType(and its implementation inTaskService) to query both due and done items. Hint: use thecheckeddate property inTaskItem; it’snilif the item is not checked. Every time a query returns a new result, that is, every time a change occurs in the database, produce an updated statistic. You’ll need to run two permanent queries for that, filter one of them to exclude either checked or unchecked item, then use thezip(_:_:resultSelector:)RxSwift operator to produce the result. - Exposing a new statistics observable in
TasksViewModel. This is a piece of cake, since you did all the hard work inTaskService. - Subscribing to this observable in
TasksViewControllerand updating the label.
To make things easier, you can define a TaskStatistics tuple typealias in TaskServiceType.swift:
typealias TaskStatistics = (todo: Int, done: Int)
You shouldn’t meet any particular difficulties in completing this challenge, aside from figuring out how to correctly filter Realm results. The interesting part is to see how you can structure new functionality and correctly spread it across the relevant components in your application.
Once you’re done with this, reuse your statistics observable to update the application badge number dynamically. This is something you want to add to the application delegate in application(_:didFinishLaunchingWithOptions:).
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.
Implementing back button support is your mission for this last challenge. You‘ll modify the project slightly to push an edit view upon regular editing, leaving creating items as a modal dialog. This is simple enough, but introduces a new challenge: how can SceneCoordinator be made aware that the current view controller is back to TasksViewController when navigating back from the edit view controller?
One solution would be to notify SceneCoordinator in the editor’s viewWillDisappear method, but this is not perfect: this method will be called when navigating either back or forward.
The solution lies in the delegate methods of UINavigationController. Your SceneCoordinator should be made a delegate of any navigation controller that comes on screen. This way, it can get notified of any new UIViewController that appears, even the transition wasn’t initiated by the scene coordinator.
To summarize your assignments for this challenge:
- Create a new
PushedEditViewModeland a matchingPushedEditTaskViewController. - Duplicate the Edit view controller in the storyboard, remove the OK and Cancel buttons, and change its class to
PushedEditTaskViewController. - Add a new scene to Scene.swift.
- Update
TasksViewModelto push this new scene instead of bringing up the modal dialog when user taps a Todo item. - Handle navigation delegate callbacks in
SceneCoordinatorto detect when a scene automatically pops.
Wow, that‘s a lot! The challenge isn’t trivial, but will help you better understand the ins and out of this architecture. Check out the solution and compare what you did. Did you get everything right? Could you find a better solution?
This concludes the final chapter of this book! We hope you loved reading it as much as we did writing it. You now have a solid foundation of programming with RxSwift (and Rx as a whole) to build on as you continue your learning. Good luck!