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 reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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.

Deciding what goes where

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

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.

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.

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.

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)
    }
.flatMapLatest(jsonProvider)
.map(Tweet.unboxMany)
.share(replay: 1)

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

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>
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)
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:

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)

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)
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)

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.

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)
}
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.

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
})

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

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

Unlock now