Chapters

Hide chapters

RxSwift: Reactive Programming with Swift

Fourth Edition · iOS 13 · Swift 5.1 · Xcode 11

24. MVVM with RxSwift
Written by Marin Todorov

RxSwift is such a big topic that this book hasn’t covered application architecture in any detail yet. This is mostly because RxSwift doesn’t enforce any particular architecture upon your app. However, since RxSwift and MVVM play very nicely together, this chapter is dedicated to the discussion of that specific architecture pattern.

Introducing MVVM

MVVM stands for Model-View-ViewModel; it’s a slightly different implementation of Apple’s poster-child MVC (Model-View-Controller).

It’s important to approach MVVM with an open mind. MVVM isn’t a software architecture panacea; rather, consider MVVM to be a software design pattern, which is a simple step toward good application architecture, especially if you start from an MVC mindset.

Background on MVC

By now you’ve probably sensed a bit of tension between MVVM and MVC. What, precisely, is the nature of their relationship? They are very similar, and you could even say they are distant cousins. But they are still different enough that an explanation is warranted.

Most of the examples in this book (and other books about programming) use an MVC pattern for the code samples.

MVC is a straightforward pattern for many simple apps and looks like this:

Each of your classes is assigned a category: the controller classes play a central role as they can update both the model and the view, while views only display data on screen and send events like gestures to the controller. Finally, the models read and write data to persist the app state.

MVC is a simple pattern that can serve you well for a while, but as your app grows you will notice that a lot of classes are neither a view, nor a model, and must therefore be controllers. A common trap to fall into is to start adding more and more code to a single controller class. Since you start with a view controller with iOS apps, the easiest thing to do is to stuff all your code into that view controller class. Hence the old joke that MVC stands for “Massive View Controller”, because the controllers can grow to hundreds or even thousands of lines.

Overloading your classes is simply a bad practice, and not necessarily a shortcoming of the MVC pattern. Case in point: Many developers at Apple are fans of MVC, and they turn out amazingly well-built macOS and iOS software.

Note: You can read more about MVC at the dedicated Apple documentation page: http://apple.co/2zKgwOR

MVVM to the rescue

MVVM looks a lot like MVC, but definitely feels better. People who like MVC usually love MVVM, as this newer pattern lets them easily solve a number of issues common to MVC.

The obvious departure from MVC is a new category named ViewModel:

ViewModel takes a central role in the architecture: It takes care of the business logic and talks to both the model and the view.

MVVM follows these simple rules:

  • Models don’t talk directly to other classes, although they can emit notifications about data changes.
  • View Models talk to Models and expose data to the View Controllers.
  • View Controllers only talk to View Models and Views as they handle view lifecycle and bind data to UI components.
  • Views only notify view controllers about events (just as with MVC).

Wait, doesn’t the View Model do exactly what the controller did in MVC? Yes… and no.

As mentioned earlier, a common issue is stuffing view controllers with code that doesn’t control the view per se. MVVM tries to solve this problem by grouping the view controller together with the view, and assign its sole responsibility of controlling the view.

Another benefit of the MVVM architecture is the increased testability of the code. Separating the view lifecycle from the business logic makes testing both the view controller and the view model very straightforward.

Last but not least, the view model is completely separated from the presentation layer and, when necessary, can be re-used between platforms. You can just replace the view–view controller pair and migrate your app from iOS to macOS or even tvOS.

Deciding what goes where

However, don’t assume that everything else should go in your View Model class.

This would be the same madness as you sometimes end up with in MVC. It’s up to you to divide and assign responsibilities in a sensible fashion across your code base. Thus, leave the View Model as the brain between your data and your screen, but make sure you split networking, navigation, cache, and similar responsibilities into other classes.

So how do you work with these extra classes, if they don’t fall under any of the MVVM categories? MVVM doesn’t enforce rules about these, but in this chapter you will work on a project that will introduce you to some possible solutions.

One good idea, which you’ll cover in this chapter, is to inject all objects a View Model needs via its init, or possibly later in its lifecycle.

This means you can pass long-living objects like stateful API classes, or persistence layer objects from view model to view model:

In the case of this chapter’s project, Tweetie, you will pass things around in that fashion, such as the object taking care of in-app navigation (Navigator), the currently-logged in Twitter account (TwitterAPI.AccountStatus), and more.

But are smaller files the only benefit of MVVM? When used properly, the pattern allows for improvements over classic MVC:

  • View controllers tend to be a lot simpler and really deserve their name because their only responsibility is to “control” the view. MVVM works especially well with RxSwift/RxCocoa since they let you bind observables to UI components, which is a key enabler for this pattern.
  • View models follow a clear Input -> Output pattern and are easy to test as they provide predefined input and testing for the expected output.

  • Visually testing view controllers becomes much easier by creating mock view models and testing for the expected view controller state.

Last but not least, since MVVM is a great departure from MVC, it also serves as an enabler and an inspiration to explore additional software architecture patterns.

Keen to try out MVVM? As you work through this chapter you’ll see many of its benefits in action.

Getting started with Tweetie

In this chapter, you will work on a multi-platform project called Tweetie. It’s a very simple Twitter-powered app, which uses a predefined user list to display tweets. By default, the starter project uses a Twitter list featuring all authors and editors of this book. If you’d like, you can easily change the list to turn the project into a sports, writing, or cinema-oriented app.

Additionally, the app has a secret superpower - in case you’re not a registered Twitter developer and don’t have access to their API, the app will run entirely from cached API data bundled in the Xcode project!

The project has macOS and iOS targets and solves a lot of real-life programming tasks by using the MVVM pattern. There is a lot of code already included with the starter project; you’ll just focus on the parts relevant to MVVM.

As you progress through this chapter, you’ll witness how MVVM provides a clear distinction between the following:

  • Code that has to do with UI and is therefore platform-specific, such as a view controller that uses UIKit for iOS, and a separate macOS-only view controller that uses AppKit.
  • Code that is reused as-is, since it doesn’t depend on the specific platform’s UI framework, such as models and view models.

Time to dive in!

Project structure

Find the starter project for this chapter, install all CocoaPods, and open the project in Xcode. Take a quick peek into the project structure before working on any code.

In the project navigator, you will find a number of folders:

  • API Cache: Cached Twitter API responses to use while you’re waiting for your Twitter Developer approval.
  • Common Classes: Shared code between macOS and iOS. Includes an Rx Reachability class extension, and extensions on UITableView, NSTableView, and more.
  • Data Entities: Data objects to use with the Realm Mobile Database in order to persist data on disk.
  • TwitterAPI: A bare bones Twitter API implementation to make requests to Twitter’s JSON API. TwitterAccount is the class that gets you an access token to use with the API, while TwitterAPI makes authorized requests to the web JSON endpoints.
  • View Models: Where the app’s three view models reside. One is fully functional and you will work on completing the other two.
  • iOS Tweetie: Contains the iOS version of Tweetie, including a storyboard and iOS view controllers.
  • Mac Tweetie: Contains the macOS target with its storyboard, assets, and view controllers.
  • TweetieTests: Where the app’s tests and mock objects reside.

Note: The tests won’t pass until you’ve completed the chapter’s challenges, and you can use the test provided to make sure you completed the challenges correctly. Don’t be surprised if things don’t work right away!

Your task is to complete the app so users can see the tweets of all users in the list.

You will start by completing the networking layer, then move on to writing a view model class, and in the end you will create the two view controllers (one for iOS and one for macOS) that use the finished view model to display data onscreen.

You’ll get to work on a number of different classes and experience MVVM first hand.

Optionally getting access to Twitter’s API

Twitter’s API is unfortunately closed so to get access to their data you need to go through a developer application process first.

Since this might take a while (or worse, your application might get rejected) we’ve bundled some API response data with the Tweetie Xcode project so you can work through this chapter entirely off that cached data without ever connecting to the Internet.

In case you want to immediately start working on the chapter’s project, jump straight to the next chapter section, “Finishing up the network layer”.

Only in the event you’d like to apply for a Twitter developer account and work through this chapter with real time Twitter data, follow these steps:

  1. Apply for a developer account at https://developer.twitter.com/en/apply/user.

  2. Once your account is ready to go, create a new app here: https://developer.twitter.com/en/apps.

  3. In the app’s details page, select the Keys and tokens tab. This is where you’ll find your API key and API secret:

  1. Now select the Permissions tab, and set the app’s permissions to Read only. Your new app will only read data, so there’s no need for additional permissions.

  2. Open the Tweetie project in Xcode, and in TwitterAPI/TwitterAccount.swift, set the values of the key and secret properties to your developer’s key and secret.

This will set up the iOS and the macOS projects with your credentials. You’re now good to work with the Twitter API!

Finishing up the network layer

The project already includes quite a lot of code. You’ve already been through a lot in this book, and we’re not going to make you work through trivial tasks such as setting up your observables and view controllers. You’ll start by completing the project networking.

The class TimelineFetcher in TimelineFetcher.swift is responsible to automatically refetch the latest tweets while the app is connected. The class is quite simple and uses an Rx timer to repeatedly invoke the subscription that fetches the JSON from the web.

TimelineFetcher has two convenience inits: one to fetch the tweets from a given Twitter list, and another to fetch a given user’s tweets.

In this section, you’ll add the code that makes a web request and maps the response to Tweet objects. You’ve already completed similar tasks in this book, so we’ve included most of that code in Tweet.swift.

Note: People often ask where to add networking when working on an MVVM project, so we’ve structured this chapter to give you the chance to add networking yourself. There’s nothing special about networking; it’s a regular class you inject into your view models.

In TimelineFetcher.swift, scroll to the bottom of init(account:jsonProvider:) and find this line (it’s just a placeholder to make the code run in the starter project):

timeline = Observable<[Tweet]>.empty()

Replace that line with the following:

timeline = reachableTimerWithAccount
    .withLatestFrom(feedCursor.asObservable()) { account, cursor in
        return (account: account, cursor: cursor)
    }

You take the timer observable reachableTimerWithAccount and combine it with feedCursor. feedCursor currently doesn’t do anything, but you’ll use this relay to store your current position in the Twitter timeline, indicating which tweets you’ve already fetched.

Xcode might display an error once you add this code, but ignore it for the moment. This will get resolved with the next code addition.

Now add the following to the chain:

.flatMapLatest(jsonProvider)
.map(Tweet.unboxMany)
.share(replay: 1)

You start by flatmapping the method parameter jsonProvider. jsonProvider is a closure that’s injected into init. Each of the convenience initializers is supposed to fetch different API endpoints, so injecting jsonProvider is a handy way to avoid using if statements or branching the logic in the main initializer init(account:jsonProvider:).

jsonProvider returns an Observable<[JSONObject]>, so the next step is to map that to an Observable<[Tweet]>. You use the provided Tweet.unboxMany method, which attempts to convert the JSON objects into an array of tweets.

With these few lines of code, you’re prepared to fetch the tweets. timeline is a public observable, so this is how your view models will access the list of latest tweets.

The app’s view models might save the tweets to disk or use them straight away to drive the app’s UI, but that’s entirely their own business.

TimelineFetcher simply fetches tweets and exposes the results:

Since this subscription is called repeatedly, you also need to store the current position (or cursor) so that you don’t fetch the same tweets over and over again. Just below the place you typed in the last piece of code, add below // Store the latest position through timeline:

timeline
  .scan(.none, accumulator: TimelineFetcher.currentCursor)
  .bind(to: feedCursor)
  .disposed(by: bag)

feedCursor is a property on TimelineFetcher of type BehaviorRelay<TimelineCursor>. TimelineCursor is a custom struct that holds the oldest and latest tweet IDs you’ve fetched so far.

In the previous code, you use scan to track the IDs. Each time you grab a new batch of tweets, you update the value of feedCursor. If you are interested in the logic of updating the timeline cursor, have a look inside TimelineFetcher.currentCursor().

Note: We won’t cover the cursor logic in detail, since it’s specific to the Twitter API. You can read more about cursoring at http://bit.ly/2zLF7mx.

Next you need to create a view model. You’ll use the completed TimelineFetcher class to grab the latest tweets from the API.

Adding a View Model

The project already includes a navigation class, data entities, and the Twitter account access class. Now that your network layer is complete, you can simply combine all of these to log the user into Twitter and fetch some tweets.

Note: If you are using the cached API data, the app always assumes the user is logged into Twitter. You can peek in TwitterAccount.swift to see how this is coded, but we won’t cover it in detail.

In this section, you won’t concern yourself with controllers. Find the project folder View Models and open ListTimelineViewModel.swift. As the name suggests, this view model will fetch the tweets of a given user list.

It’s good practice (but certainly not the only way) to clearly define three sections in your view model code:

  1. Init: In which you define one or more inits where you inject all your dependencies.
  2. Input: Contains any public properties, such as plain variables or RxSwift subjects/relays, which allow the view controller to provide input.
  3. Output: Contains any public properties (usually Observables or Drivers), which provide the output of the view model. These are usually lists of objects to drive a table or collection view, or any other type of data a view controller would use to drive the app’s UI.

ListTimelineViewModel has a bit of code already in its init that initializes the fetcher property. fetcher is an instance of TimelineFetcher for fetching tweets.

Time to add more properties to the view model. First, add the following two properties, which are neither input nor output, but simply help you persist the injected dependencies:

let list: ListIdentifier
let account: Driver<TwitterAccount.AccountStatus>

Since those are constants, your only chance to initialize them is in init(account:list:apiType). Insert the following at the top of the class initializer:

self.account = account
self.list = list

Now you can move on to adding the input properties. But what properties should those be, since you’ve already injected all the dependencies of this class? The injected dependencies and the parameters you provide to init allow you to provide input at initialization time. Other public properties will allow you to provide input to the view model at any time through its lifetime.

For example, consider an app that lets the user search a database. You would bind the search text field to an input property of the view model. As the search term changes, the view model will search the database and change its output accordingly, which in turn will be bound to a table view to show the results.

For the current view model, the only input you will have is a property that lets you pause and resume the timeline fetcher class. TimelineFetcher already features a BehaviorRelay<Bool> to do just that, so you’ll need a proxy property in the view model.

Insert the code below in the input section of ListTimelineViewModel, as marked with the handy comment // MARK: - Input:

var paused: Bool = false {
  didSet {
    fetcher.paused.accept(paused)
  }
}

This property is simply a proxy which sets the value of paused on the fetcher class.

Now you can move on to the view model’s output. The view model will expose the fetched list of tweets and the logged-in status. The former will be an observable sequence of tweet objects, loaded from Realm; the latter, a Driver<Bool>, will simply emit false or true to indicate whether the user is currently logged into Twitter.

In the output section (marked by a comment), insert these two properties:

private(set) var tweets: Observable<(AnyRealmCollection<Tweet>, RealmChangeset?)>!
private(set) var loggedIn: Driver<Bool>!

tweets contains the list of the latest Tweet objects. Before any tweets are loaded, such as the point before the user has logged into their Twitter account, the default value will be nil. loggedIn is a Driver, which you will initialize later on.

Now you can subscribe to TimelineFetcher’s result and store the tweets into Realm. This is, of course, quite easy when using RxRealm. Append to init(account:list:apiType:):

fetcher.timeline
  .subscribe(Realm.rx.add(update: .all))
  .disposed(by: bag)

You subscribe to fetcher.timeline, which is of type Observable<[Tweet]>, and bind the result (an array of tweets) to Realm.rx.add(update:). Realm.rx.add persists the incoming objects into the app’s default Realm database.

The last piece of code takes care of the influx of data in your view model, so all that’s left is to build the view model’s output. Find the method named bindOutput, and insert below // Bind tweets:

guard let realm = try? Realm() else {
  return
}
tweets = Observable.changeset(from: realm.objects(Tweet.self))

You see how easily you can create an observable sequence with the help of Realm’s Results class. In the code above, you create a result set out of all persisted tweets and subscribe for changes to that collection. You expose the tweets observable to interested parties, which is usually your view controller.

Next, you need to take care of the loggedIn output property. This one is simple enough to take care of — you’ll subscribe to account and map its elements to either true or false. Append to bindOutput:

loggedIn = account
  .map { status in
    switch status {
    case .unavailable: return false
    case .authorized: return true
    }
  }
  .asDriver(onErrorJustReturn: false)

This is all the view model needs to do! You took care to inject all dependencies in the init, you added some properties to allow other classes to provide input, and finally you bound the view model’s results to public properties that other classes can observe.

As you can see, the view model doesn’t know anything about the view controllers, the views, or other classes that aren’t injected via its initializer. Since the view model is so well isolated from the rest of the code, you can proceed to write its tests to make sure it works fine — even before you see any output on screen.

Adding a View Model test

In Xcode’s project navigator, open the TweetieTests folder. Inside it, you’ll find a few files provided for you:

  • Mocks/TestData.swift: Features some test JSON, and test objects.
  • Mocks/TwitterTestAPI.swift: A Twitter API mock class that tracks which methods were called and records the API responses.
  • Mocks/TestRealm.swift: A test Realm configuration that ensures Realm uses a temporary in-memory database for the tests.

Open ListTimelineViewModelTests.swift to add some new tests. The class already has a utility method to create a fresh instance of ListTimelineViewModel and two tests:

  1. test_whenInitialized_storesInitParams(), which tests if the view model persists its injected dependencies.

  2. test_whenInitialized_bindsTweets(), which checks if the view model exposes the latest persisted tweets via its tweets property.

To complete the test case, you’ll add one final test: the one to check if the loggedIn output property reflects the account authentication status. Add the following inside the class body:

func test_whenAccountAvailable_updatesAccountStatus() {

}

Since this is an asynchronous test you will use RxBlocking. You learned about that handy library in Chapter 16, “Testing with RxTest”.

You will test the elements emitted by your view model’s loggedIn property, and so you tell the observer to listen for Bool elements.

Now add the following:

let accountSubject = PublishSubject<TwitterAccount.AccountStatus>()
let viewModel = createViewModel(accountSubject.asDriver(onErrorJustReturn: .unavailable))

Next, you create a PublishSubject, which you will use to emit test AccountStatus values. You pass the subject to createViewModel() and finally fetch a view model instance, all ready and set up for the test.

Add:

let loggedIn = viewModel.loggedIn.asObservable().materialize()

Now that your subscription is in place, you can emit few test values.

Add the following async block:

DispatchQueue.main.async {
  accountSubject.onNext(.authorized(AccessToken()))
  accountSubject.onNext(.unavailable)
  accountSubject.onCompleted()
}

Finally, subscribe to loggedIn, take 3 events and check if they are the ones you expect:

let emitted = try! loggedIn.take(3).toBlocking(timeout: 1).toArray()

XCTAssertEqual(emitted[0].element, true)
XCTAssertEqual(emitted[1].element, false)
XCTAssertTrue(emitted[2].isCompleted)

This code waits asynchronously for three events and then checks if the recorded events were the exact sequence of .next(true), .next(false), and .completed.

With that, the test case is complete. The highly isolated view model class lets you easily inject mock objects and simulate input. Read through the rest of the test suite class to see what else is being tested. If you figure out some new tests that would be useful, feel free to add them in!

Note: Since the view models in the Tweetie project are so well-isolated from the rest of the app’s infrastructure, you don’t need to run the entire app to run a test. Peek into iOS Tweetie/AppDelegate.swift to see how the code avoids creating the app’s navigation and view controllers during testing. Alternatively, you might disable the host app in testing altogether.

Now you have a fully functioning view model, which is also under test. You could run the app right now, but nothing interesting would happen. Now that we have a functioning view model, it’s time to make use of it!

Adding an iOS view controller

In this section, you’ll write the code to wire your view model’s output to the views in ListTimelineViewController — the controller that will display the combined tweets of users in the preset list.

First, you’ll work on the iOS version of Tweetie. In the project navigator, open the folder iOS Tweetie/View Controllers/List Timeline. Inside you will find the view controller and iOS-specific table cell view files.

Open ListTimelineViewController.swift and have a quick look. The ListTimelineViewController class features a view model property and a Navigator property. Both classes are injected through the createWith(navigator:storyboard:viewModel) static factory method.

You’ll add two sets of setup code to the view controller. One will be some static assignments in viewDidLoad(), and the other will be bindings of the view model to the UI in bindUI().

Add the code below to viewDidLoad(), before the call to bindUI():

title = "@\(viewModel.list.username)/\(viewModel.list.slug)"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .bookmarks, target: nil, action: nil)

This will set the title to the list’s name and create a new button on the right-hand side of the navigation item.

Next, on to binding the view model. Insert this into bindUI(), under // Bind button to the people view controller:

navigationItem.rightBarButtonItem!.rx.tap
  .throttle(.milliseconds(500), scheduler: MainScheduler.instance)
  .subscribe(onNext: { [weak self] _ in
    guard let self = self else { return }
    self.navigator.show(segue: .listPeople(self.viewModel.account, self.viewModel.list), sender: self)
  })
  .disposed(by: bag)

You subscribe to taps on the right bar item and throttle them to prevent any double taps. Then you call the show(segue:sender:) method on the navigator property to show your intent to present the segue to the screen. The segue displays the list of people: members of the selected Twitter list.

Navigator takes care to either present the requested screen, or discard your intent if it decides to do so, as it might decide to ignore your intent to present the desired view controller based on other parameters.

Note: Read through the Navigator class definition for more details about the class implementation. It contains the list of all possible navigable screens, and you can invoke these segues only by providing all required input parameters.

You also need to create another binding to display the latest tweets in the table view. Scroll to the top of the file and import the following library to easily bind RxRealm results to table and collection views:

import RxRealmDataSources

Then go back to bindUI() and append under // Show tweets in table view:

let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier:
  "TweetCellView", cellType: TweetCellView.self) { cell, _, tweet in
    cell.update(with: tweet)
}

dataSource is a table view data source, specifically suited to drive a table view from an observable sequence that emits Realm collection changes. In a single line, you configure the data source completely:

  1. You set the model type as Tweet.
  2. Then you set the cell reuse identifier to TweetCellView.
  3. Finally, you provide a closure to configure each cell before it shows on screen.

You can now bind the data source to the view controller’s table view. Add this code under the last block:

viewModel.tweets
  .bind(to: tableView.rx.realmChanges(dataSource))
  .disposed(by: bag)

Here you bind viewModel.tweets to realmChanges and provide the preconfigured data source. This is the bare minimum you need to drive the table view with animated changes.

The final binding for this view controller will show or hide the message on top depending on whether the user has logged in to Twitter or not. Append the following under // Show message when no account available:

viewModel.loggedIn
  .drive(messageView.rx.isHidden)
  .disposed(by: bag)

This binding toggles messageView.isHidden based on the current loggedIn value.

Note: If you’re working off the cached API data, the user account will always be logged in, but binding viewModel.loggedIn should be working just fine and hide the messageView banner as soon as you start the app.

This section showed you why bindings are a key enabler of the MVVM pattern. With your view controllers serving only as “glue” code, you can easily separate out concerns really easily. Your view model remains mostly ignorant about the current platform it runs on, as it doesn’t import any UI framework as UIKit or Cocoa.

Run the app and observe all the bindings your shiny new view model drives:

As soon as the app completes the JSON request (if you’re using Twitter’s actual API), the message at the top will disappear. Then the fetched tweets will “pour in” with a snappy animation.

Finally, when you tap on the bar item on the right side, the app will take you to the users list view controller:

And that’s that! In the next section, you will learn how easy it is to reuse your view model across platforms.

Adding a macOS view controller

The view model doesn’t know anything about the view or the view controller that uses it. It that sense, the view model could be platform-agnostic when necessary. The same view model can easily provide the data to both iOS and macOS view controllers.

ListTimelineViewModel is precisely one such view model. Its only dependencies are RxSwift, RxCocoa, and the Realm database. Since those libraries are cross-platform themselves, the view model itself is cross-platform too.

You job is to switch to the macOS target of the Xcode project and build a view controller that mirrors the iOS one you built above.

From Xcode’s scheme selector, choose MacTweetie/My Mac and run the project to see what the macOS starter project looks like.

The app displays the list of all accounts included in the pre-defined Twitter list, but the right-hand side of the window remains empty. The blank view controller is the one that should be displaying the tweets timeline. When complete, it should look much like the tweet list you created for the iOS Tweetie app.

Open Mac Tweetie/ViewControllers/List Timeline and select ListTimelineViewController.swift. The file is named similarly to the iOS view controller file, but is located in the Mac Tweetie folder instead.

Start by displaying the name of the list at the top, just as you did in the iOS app. Add the following to viewDidLoad(), before the call to bindUI():

NSApp.windows.first?.title = "@\(viewModel.list.username)/\(viewModel.list.slug)"

Now you can move on to the bindings. If you skim through the code of the macOS view controller, you’ll notice it uses the same view model and navigator classes as its iOS counterpart. That’s great news, since you already know (and love) ListTimelineViewModel.

The view controller code is, in fact, almost identical to the iOS version! This code similarity is one of the many benefits of RxSwift. A lot of Rx code looks quite similar between languages as well. You will likely be amazed at the ease with which you can can read and understand Java written with RxJava, or JavaScript if it’s written using RxJS.

Much like for the iOS view controller, scroll up the current file and import RxRealmDataSources:

import RxRealmDataSources

Now scroll down to bindUI(). To bind the view model’s tweets to the table view, add under // Show tweets in table view:

let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier: "TweetCellView", cellType: TweetCellView.self) { cell, row, tweet in
  cell.update(with: tweet)
}

Here you create a data source containing Tweet objects with a cell with identifier TweetCellView and configure each cell before it’s reused by calling its update(with:). On to creating the table view binding.

You create a binding between the table view rows and Realm changes by using the already initialized data source object.

Now you can simply bind the view model’s tweets property to the configured binding. Add the following:

viewModel.tweets
  .bind(to: tableView.rx.realmChanges(dataSource))
  .disposed(by: bag)

This binding should bring the table view to life. Run the app and observe :trollface: the tweets showing up in the right hand side of the window.

Is this the real life — or is this just fantasy? You didn’t have to perform any networking, data transformation, or JSON validation?

Nope — you’re working on the view controller and not on any other part of your app. The view model takes care of everything, so the only thing you needed to do was to bind the data to the UI.

You now have a basic understanding of how to split your code into a model, a view model, and a view with a view controller. MVVM certainly has benefits over MVC for anything beyond simple apps, but it’s important to remember that MVVM isn’t the only option out there.

MVVM is a particularly sweet pattern to use with RxSwift, since Rx makes creating bindings a straightforward task. This leads to cleaner code that is easier to read and test.

Other architectural patterns have different benefits, and there might be other libraries that suit those patterns better. But if you see MVVM + RxSwift as something you might want to learn, then definitely try out the challenges below!

Challenges

Challenge 1: Toggle “Loading…” in members list

On the screen displaying the users list, the Loading… label is always visible. It’s useful to have the loading indicator there, but you really only want it to be visible while the app is fetching JSON from the server.

To complete this challenge, you will work on both the iOS and macOS apps.

First open ListPeopleViewController.swift in the iOS part of the project. In bindUI(), subscribe to viewModel.people, convert it to a Driver and map the elements to true and false. Emit false when viewModel.people is nil. Drive messageView.rx.isHidden with the resulting Driver<Bool>.

In the end you should see “Loading…” only when the app is fetching the JSON. Once it’s completed, the label should disappear automatically.

Once you’re happy with the result in the iOS app, move on to the macOS target. Since the view controller outlets have the same names, you can copy the code directly from the iOS view controller into the macOS app’s ListPeopleViewController.swift.

Challenge 2: Über challenge — Complete View Model and View Controller for the user’s timeline

You’ve noticed that there is still a part missing in both the iOS and macOS app. If you select a user from the users list, you’ll see a new, empty view controller appear.

As the über challenge in this chapter, you will finish the two apps in the project and display the personal Twitter timelines of selected users. If you want to try completing this challenge on your own follow the instructions below, otherwise the challenge folder for this chapter includes a solution, which you can read through.

In PersonTimelineViewModel.swift, you will find a property named tweets. Change this to a lazy variable and use the following code to initialize it.

return self.fetcher.timeline
  .asDriver(onErrorJustReturn: [])
  .scan([], accumulator: { lastList, newList in
  return newList + lastList
})

This code subscribes to the class’ TimelineFetcher instance and gathers all emitted tweets in a list. In PersonTimelineViewModelTests.swift, you’ll find a test case for the tweets property, which you can now un-comment.

Then switch to the iOS PersonTimelineViewController.swift, scroll to bindUI() and add two subscriptions to viewModel.tweets.

  • With the first subscription, drive the rx.title of the view controller. Display “None found” before you fetch the tweets along with the username of the user (from the viewModel) when the tweets show up.
  • For the second subscription, get a data source object by using the provided createTweetsDataSource(), then map the tweets to a single TweetSection (consult the RxDataSources chapter if you need help), and drive the table.

For the macOS version of the app (in the corresponding PersonTimelineViewController.swift), use the provided tweets array property. Subscribe viewModel.tweets, update the tweets array and reload the table. You can optionally update the window title just as you did for the iOS app.

Now you should be able to open the user’s list, select a user and see their personal tweet timeline appear in the app like so:

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.
© 2026 Kodeco Inc.