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

Heads up... You’re accessing parts of this content for free, with some sections shown as qwjogknox text.

Heads up... You’re accessing parts of this content for free, with some sections shown as wwbiwgvud text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as bvdykpvux text.

Heads up... You’re accessing parts of this content for free, with some sections shown as rpxovnpon text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as trtybzsuw text.

Heads up... You’re accessing parts of this content for free, with some sections shown as wnfaqdwol text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

Deciding what goes where

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

Heads up... You’re accessing parts of this content for free, with some sections shown as wrlolcqah text.

Heads up... You’re accessing parts of this content for free, with some sections shown as rrhowrled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as ldwygxtem text.

Heads up... You’re accessing parts of this content for free, with some sections shown as lfpohttom text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as ftxinfzet text.

Heads up... You’re accessing parts of this content for free, with some sections shown as jbwafmreh text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as czcugzxes text.

Heads up... You’re accessing parts of this content for free, with some sections shown as jmpegnzoj text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

timeline = Observable<[Tweet]>.empty()
timeline = reachableTimerWithAccount
    .withLatestFrom(feedCursor.asObservable()) { account, cursor in
        return (account: account, cursor: cursor)
    }

Heads up... You’re accessing parts of this content for free, with some sections shown as wjxarmlyl text.

Heads up... You’re accessing parts of this content for free, with some sections shown as vwdavhwil text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

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

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

Heads up... You’re accessing parts of this content for free, with some sections shown as fsmawtzos text.

Heads up... You’re accessing parts of this content for free, with some sections shown as ksfyjdjyb text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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

Heads up... You’re accessing parts of this content for free, with some sections shown as xlkeqbjix text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qvdinjxel text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
self.account = account
self.list = list
var paused: Bool = false {
  didSet {
    fetcher.paused.accept(paused)
  }
}
private(set) var tweets: Observable<(AnyRealmCollection<Tweet>, RealmChangeset?)>!
private(set) var loggedIn: Driver<Bool>!
fetcher.timeline
  .subscribe(Realm.rx.add(update: .all))
  .disposed(by: bag)

Heads up... You’re accessing parts of this content for free, with some sections shown as dflofvfyd text.

Heads up... You’re accessing parts of this content for free, with some sections shown as qppusfnex text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
guard let realm = try? Realm() else {
  return
}
tweets = Observable.changeset(from: realm.objects(Tweet.self))
loggedIn = account
  .map { status in
    switch status {
    case .unavailable: return false
    case .authorized: return true
    }
  }
  .asDriver(onErrorJustReturn: false)

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:

Heads up... You’re accessing parts of this content for free, with some sections shown as kktuvqpyx text.

Heads up... You’re accessing parts of this content for free, with some sections shown as rdvigtdif text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
func test_whenAccountAvailable_updatesAccountStatus() {

}
let accountSubject = PublishSubject<TwitterAccount.AccountStatus>()
let viewModel = createViewModel(accountSubject.asDriver(onErrorJustReturn: .unavailable))
let loggedIn = viewModel.loggedIn.asObservable().materialize()
DispatchQueue.main.async {
  accountSubject.onNext(.authorized(AccessToken()))
  accountSubject.onNext(.unavailable)
  accountSubject.onCompleted()
}
let emitted = try! loggedIn.take(3).toBlocking(timeout: 1).toArray()

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

Heads up... You’re accessing parts of this content for free, with some sections shown as rrqagksiv text.

Heads up... You’re accessing parts of this content for free, with some sections shown as khneqxdeh text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

title = "@\(viewModel.list.username)/\(viewModel.list.slug)"
navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .bookmarks, target: nil, action: nil)
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)

Heads up... You’re accessing parts of this content for free, with some sections shown as pvjuzrtep text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gcpabfneg text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now
import RxRealmDataSources
let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier:
  "TweetCellView", cellType: TweetCellView.self) { cell, _, tweet in
    cell.update(with: tweet)
}
viewModel.tweets
  .bind(to: tableView.rx.realmChanges(dataSource))
  .disposed(by: bag)
viewModel.loggedIn
  .drive(messageView.rx.isHidden)
  .disposed(by: bag)

Heads up... You’re accessing parts of this content for free, with some sections shown as hzrymzhul text.

Heads up... You’re accessing parts of this content for free, with some sections shown as cdrordxis text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as lxqusdfov text.

Heads up... You’re accessing parts of this content for free, with some sections shown as bspatgjol text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

NSApp.windows.first?.title = "@\(viewModel.list.username)/\(viewModel.list.slug)"
import RxRealmDataSources
let dataSource = RxTableViewRealmDataSource<Tweet>(cellIdentifier: "TweetCellView", cellType: TweetCellView.self) { cell, row, tweet in
  cell.update(with: tweet)
}

Heads up... You’re accessing parts of this content for free, with some sections shown as lxbynxber text.

Heads up... You’re accessing parts of this content for free, with some sections shown as sgwygrdit text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

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

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.

Heads up... You’re accessing parts of this content for free, with some sections shown as jntemzruc text.

Heads up... You’re accessing parts of this content for free, with some sections shown as gspolsxos text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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.

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

Heads up... You’re accessing parts of this content for free, with some sections shown as qzqoxfmyg text.

Heads up... You’re accessing parts of this content for free, with some sections shown as phlinslal text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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

You’re accessing parts of this content for free, with some sections shown as rspoctjev text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now