RxSwift_v1.0笔记——23 MVVM with RxSwift

RxSwift_v1.0笔记——23 MVVM with RxSwift

RxSwift是一个很大的话题,本书之前没有覆盖任何应用构架的细节。是因为RxSwift不会强迫在你的应用上使用任何特定的构架。不过,因为RxSwift与MVVM一起工作更合适,本章将专注于讨论讨论特殊的构架样式。

Introducing MVVM 353

MVVM代表了Model-View-ViewModel;它与Apple的亲儿子MVC有略微不同的实现。

用一个开放的思想来处理MVVM是很重要的。MVVM不是软件构架的万能药;当然,考虑到MVVM是一个软件范式,使用它是朝着好的应用构架迈出的第一步,尤其是你开始是MVC的思维方式。

Some background on MVC 354

现在你对MVVM和MVC可能感觉有一点矛盾(tension)。它们之间有什么联系?它们非常相似,甚至你可以认为它们是远房亲戚。但是解释它们之间的不同点任然是必要的。

在本书(和其他关于编程的书)中的大部分的例子使用MVC样式来写代码示例。MVC是对许多简单的app来说是简单的样式,它看起来像这样:

每个类分配一个类别:controller类扮演中间角色让model和view能够更新,views仅仅在屏幕上显示数据并发送事件(如手势)到controller。最后models读和写数据来固化app状态。

MVC是简单的样式,它能暂时(for a while)为你服务,但是当你的app成长后,你将注意到许多类既不是view又不是model,所以只能是controllers。你开始调入一个普遍的陷阱,在一个controller类中增加了越来越多的代码。由于你是从iOS应用程序启动视图控制器,最简单的方法是将所有代码放入该视图控制器类。因此,MVC代表“Massive View Controller”的老笑话,因为控制器可以成长为数百甚至数千条行。

过载你的类是一个不好的做法,但不一定是MVC模式的缺点。例如:苹果的许多开发人员都是MVC的粉丝,他们生产(turn out)了非常好的macOS和iOS软件。

Note:你可以阅读更多关于MVC在苹果专用的文档页面:https://developer.apple.com/library/content/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

MVVM to the rescue 354

MVVM看起来很像MVC,但一定感觉更好。喜欢MVC的人通常也喜欢MVVM,这个新的样式让他们更容易解决许多MVC普遍的问题。

与MVC明显不同的是一个叫ViewModel的新种类:

ViewModel在构架中作为一个核心角色:它负责业务逻辑并与模型和视图进行对话。

MVVM有以下简单的规则:

  • Models不直接与其他类对话,但是他们能发射关于数据变化的通知。
  • View ModelsModels对话并暴露数据给View Controllers
  • View Controllers仅仅与View ModelsViews会话,他们处理视图的生命周期并绑定数据到UI组件。
  • Views仅仅通知事件给视图控制器(就像MVC一样)。

等等,View Model不正是做了MVC中控制器做的事吗?是,也不是。

正如先前所说的,普遍的问题是视图控制器塞入了不是控制视图的代码。MVVM通过把视图控制器与视图组合来尝试解决这个问题,并让它只负责控制视图。

MVVM构架的另一个好处是增加代码的可测试性。把视图的生命周期从业务逻辑分离,让测试视图控制器和视图模型变的非常简单。

最后但并非最不重要的是,视图模型完全从显示层分离,当需要的时候,能够在不同平台间重用。你可以仅仅替换视图/视图控制器对,然后迁移你的app从iOS到macOS,甚至是tvOS。

What goes where? 355

但是,不要以为一切都应该在你的View Model类中。

这与你最终在MVC一样,将是同样愚蠢的。您可以基于你的代码来明确划分和分配责任。因此,留着View Model作为数据和屏幕之间的大脑,但是确保你分离了网络,导航,缓存和相似的职责到其他类。

如果它们不属于任何MVVM类别,这些额外的类如何处理呢?MVVM对于这些没有硬性规定,但是在本章你将工作的项目,它将给你介绍一些可行的解决方案。

本章将介绍一个好方法,它将通过其初始化或尽可能在其生命周期之后,注入View Model所需的所有对象。这就是说你能够将长时间活动的对象,像是API类的状态或来自视图模型的固化层对象到另一个视图模型:

在本章的项目“Tweetie”中,您将以这种方式传递一些东西,例如关于应用内导航(Navigator)的对象,当前登录的Twitter帐户(TwitterAPI.AccountStatus)等等。

但是MVVM的唯一好处是让代码变的更短吗?如果使用得当,MVVM比MVC有更多优势:

  • 视图控制器趋于更简单和名副其实,因为它的唯一责任是控制视图。MVVM更易于使用RxSwift/RxCocoa,因为能够绑定observables到UI组件是MVVM的关键能力。
  • 视图模型有清晰的Input -> Output样式,并且在为预期输出提供预定义的输入和测试时非常容易测试。
  • 通过创建模拟视图模型并对预期的视图控制器状态进行测试,形象化地测试视图控制器变得更加容易。

最后但并非最不重要的一点,因为MVVM是一个伟大的分离至MVC,它也可以作为一个启发和灵感来探索更多的软件架构模式。

想试试MVVM? 在您阅读本章时,您将看到其许多好处。

Getting started with Tweetie 357

本章,你将工作在叫做Tweetie的多平台项目。它是一个非常简单的Twitter-powered应用,它使用一个预定义的用户列表来向用户显示推文。默认情况下,起始程序项目使用的是具有(featuring)本书所有作者和编辑者的Twitter列表。如果你喜欢,你可以很容易的改变这个列表来转换项目为运动,写作,摄影app。

这个项目面向macOS和iOS,并通过使用MVVM样式来解决许多现实生活(real-life)中的编程任务。有许多代码包含在了起始项目中,你将聚焦在MVVM相关部分。

当你完成本章后,你将见证MVVM有助于区分以下内容:

  • 与UI相关的代码是特定于平台的,例如为iOS的视图控制器使用UIKit ,以及分离的macOS独有的视图控制器使用Cocoa。
  • 代码按原样重用,因为它不依赖于特定平台的UI框架,例如模型和视图模型中的所有代码。

是时候潜入了!

Project structure 357

安装所有CocoaPods,打开项目,预览下项目结构。

在项目导航中,你将发现有很多文件夹:

  • Common Classes:在macOS与iOS间共享代码。包含一个RxReachability类扩展,在 UITableView, NSTableView上的扩展等等
  • Data Entities:为了固话数据到硬盘,数据对象使用Realm移动数据库。
  • TwitterAPI:一个轻量的API实现来向Twitter’s JSON API发送请求。 TwitterAccount是允许您访问用户设备上登录的Twitter帐户的类,而TwitterAPI会向Web JSON端点发出请求。
  • View Models:三个app的视图模型位于这里。一个是功能完整的,你将完成另外两个。
  • iOS Tweetie: 包含iOS版本的Tweetie,包括一个storyboard和iOS视图控制器。
  • Mac Tweetie:包含macOS目标与它的storyboard,资源和视图控制器。
  • TweetieTests:app测试和模拟对象位于此。
Note:直到你完成了章节挑战测试才会通过,并且你能够使用测试来确保正确完成挑战。如果现在不工作,不用惊讶!

你的任务是完成app以便用户能够看到在列表中所有用户的tweets。你首先实现网络层,然后写一个视图模型类,并在最后你将创建两个视图控制器(一个给iOS,另一个给macOS),使用完成的视图模型在屏幕上显示数据。

您将会参加许多不同的课程,并亲身体验MVVM。

Finishing up(完成) the network layer 359

这个项目已经包含了许多代码。您在本书中已经学了很多,我们不会来实现简单的任务,例如设置observables和视图控制器。你将开始完成项目的网络层。

TimelineFetcher.swift中的类 TimelineFetcher是负责在app连接时抓取最新的tweets。这个类很简单,并且使用了一个Rx定时器来重复调用抓取来至web的JSON的订阅。

TimelineFetcher有两个遍历的测试:一个用来抓取给定Twitter列表的推文(tweets),另一个抓取给定用户的推文。

在这个章节,你将增加代码来做网络请求并映射响应到Tweet对象。在本书中你已经完成过相似的任务,因此在Tweet.swift中已经包含了大部分代码。

Note:人们常常会问当使用MVVM做项目时在哪里增加网络层,因此我们编写了这章让你有机会自己增加网络层。关于网络层没有什么是难以理解的;它是一个你注入视图模型的常用类。

TimelineFetcher.swift, 滚动到 init(account:jsonProvider:)的底部,找到这行:

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

用以下内容替换那行:

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

您可以使用定时器observable reachableTimerWithAccount并将其与feedCursor组合。 feedCursor当前没有做任何事,但是您将使用此变量把你当前的位置存储在Twitter时间轴中,来指明您已经获取的哪些推文。

一旦你增加这个代码,Xcode会显示一个错误,现在可以忽略它。这将在增加后续代码后解决。

现在增加下面内容到链:

.flatMapLatest(jsonProvider)
.map(Tweet.unboxMany)
.shareReplayLatestWhileConnected()

您首先将参数jsonProvider进行flatmapping。 jsonProvider是注入到init的闭包。每个便利inits都支持抓取不同的API端点,因此注入 jsonProvider是一个便利的方式来避免在主初始化程序 init(account:jsonProvider:)中使用if声明或分支逻辑。

jsonProvider返回一个 Observable<[JSONObject]>,因此下一步是map到一个 Observable<[Tweet]>。你使用已提供的 Tweet.unboxMany函数,尝试转换JSON对象到tweets数组中。

用这些新的代码,你准备抓取tweets了。 timeline是一个公共的observable,这就是你的视图模型如何来访问最新tweets的列表。app的视图模型可能存储了推文到硬盘或马上(straight away)使用它们驱动app的UI,但是那完全是它们自己的事。 TimelineFetcher简单的抓取推文并显示结果:

因为这个订阅被重复的调用,你也需要存储当前位置(或光标)以便你不会重复抓取同样的推文。接着在你输入的下面增加:

timeline
  .scan(.none, accumulator: TimelineFetcher.currentCursor)
  .bindTo(feedCursor)
  .addDisposableTo(bag)

feedCursor是在TimelineFetcher上的Variable<TimelineCursor>类型的属性。 TimelineCursor是一个自定义结构体,它保存了迄今你已经抓取的最新和最老的推文ID。每次你抓取一组新的推文,你就更新 feedCursor的值。如果你对更新timeline cursot,的逻辑感兴趣,请查看 TimelineFetcher.currentCursor()。

Note:本书不覆盖cursor的详细逻辑方面的知识,因为他是专用于Twitter API的。你可以读取更多关于cursoring的内容在:https://dev.twitter.com/overview/api/cursoring

下一步你需要创建一个视图模型。你将使用完成的 TimelineFetcher类从API抓取最新推文。

Adding a View Model 361

本项目已经包含了一个导航类,数据实体,和Twitter账号访问类。现在你的网络层已经完成,你可以简单的合并所有这些给Twitter的登录用户,然后抓取一些推文。

在本节,你不用关心控制器。找到项目的View Models文件夹,打开ListTimelineViewModel.swift。作为同样的建议,视图模型将抓取给定用户列表的推文。

它是一个很好的实践(但确定不是一个唯一的方式)来澄清在你的视图模型代码的三个部分的定义:

  1. Init:在这里你定义一个或多个inits来注入你所有的依赖。
  2. Input:包含任何公共属性,例如简单(plain)变量或RxSwift主题,它允许视图控制器提供输入。
  3. Output:包含任何公共属性(通常是observables),它提供视图模型的输出。通常有对象的列表来驱动一个表格或集合视图,或者是一个视图控制器用来驱动app的UI的其他类型的数据。

ListTimelineViewModel的初始化里已经有少许代码用来初始化 fetcher属性。 fetcher是 TimelineFetcher的一个实例,它用来抓取推文。

是时候来增加更多的属性到视图模型了。首先,增加下面两个属性,他们既不是输入又不是输出,但它简单的帮助你持有注入的依赖:

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

由于他们是常量,你的唯一初始化他们的机会是在 init(account:list:apiType)中。在初始化类顶部插入下面代码:

self.account = account
self.list = list

现在你能够继续增加输入属性。既然你已经注入了所有这个类的依赖,然而什么属性应该做输入呢?注入依赖和你提供给init的参数允许你在初始化时提供给输入。其他公共属性将允许你在它生命周期的任何时候,提供输入给视图模型。

例如,考虑一个让用户搜索数据库的app。你将绑定搜索文本框到视图模型的输入属性。当搜索词改变,视图模型将响应地搜索数据库并改变他的输出,它将依次(in turn)绑定到表格视图来显示结果。

为当前的视图模型,你拥有的唯一输入是一个属性,它让你暂停和恢复timeline fetcher类。 TimelineFetcher已经具有(feature)一个 Variable<Bool>来做到这一点,所以在视图模型中你需要一个代理属性。

在 ListTimelineViewModel输入部分,用方便的注释 // MARK: - Input标记的位置,插入下面代码:

var paused: Bool = false {
didSet {
​ fetcher.paused.value = paused
}

这个属性是一个简单的代理,它在fetcher类上设置 paused的值。

现在你能够继续做视图模型的输出了。视图模型将显示推文的抓取列表和登录状态。前者将是从Realm加载的 Variable的推文对象;后者,一个 Driver<Bool>简单的发射false或true来标识是否用户正确的登录到Twitter。

在输出部分(通过注释标记),插入下面两个属性:

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

tweets包含最新 Tweet对象的列表。在任何推文被加载前,例如在用户登录了他们的Twitter账号前,默认值是nil。 loggedIn

是一个Driver,它将在稍后被除数。

现在你能够订阅 TimelineFetcher的结果,并存储推文到Realm。当你使用RxRealm时,这当然是非常容易的。附加到 init(account:list:apiType:):

fetcher.timeline
  .subscribe(Realm.rx.add(update: true))
  .addDisposableTo(bag)

你订阅到 fetcher.timeline,它是 Observable<[Tweet]>类型的,然后绑定结果(tweets的数组)到 Realm.rx.add(update:)。 Realm.rx.add固化输入的对象到app默认的Realm数据库中。

最后一段代码关注在你视图模型中的数据流入,所以剩下的就是构建视图模型的输出。 找到名为bindOutput的方法,然后插入:

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

当你学习了21章,“RxRealm”,你可以容易的用Realm的Resultes辅助类来创建一个observable序列。在上面的代码中,您可以从所有持久化的推文中创建一个结果集,并订阅该集合的更改。你呈现感兴趣的部分推文observable,它通常是你的试图控制器。

下一步你需要考虑loggedIn输出属性。这个很容易照顾——你仅仅需要订阅账号并映射它的元素到true或false。附加下面内容到bindOutput:

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

这是所有视图模型需要做的!你小心的注入所有依赖到init内,你增加一些属性来允许其他类提供输入,最后你绑定视图模型的结果到公共属性,这样其他类就能够观察。

正如你看到的,视图模型不知道任何关于视图控制器,视图,或其他类的内容,它们不会通过视图模型的初始化注入。因为视图模型很好的隔离了剩余的代码,你能够继续写他的测试来确保它正常工作——甚至在你在屏幕上看到任何输出之前。

Adding a View Model test 364

在Xcode项目导航内,打开TweetieTests文件夹。在这里面你将找到给你提供的一些东西:

  • TestData.swift:提供一些测试JSON和测试对象。
  • TwitterTestAPI.swift:Twitter API模拟(mock)类,这个方法调用并记录了API响应。
  • TestRealm.swift:为了测试,使用一个测试Realm配置确保了Reaml使用一个零时的内存数据库。

打开ListTimelineViewModelTests.swift,增加一些新的测试。这个类已经有一个实用的方法来创建一个新的 ListTimelineViewModel实体和两个测试:

  1. test_whenInitialized_storesInitParams(),它测试视图模型是否固化它注入的依赖。
  2. test_whenInitialized_bindsTweets(),通过它的 tweets属性,它检查视图模型是否显示最新固化的推文。

为了完成测试用例,你将增加一个新的测试:一个用来检测是否 loggedIn输出属性属性反应了账号的鉴定状态。增加下面代码:

func test_whenAccountAvailable_updatesAccountStatus() {
  let asyncExpect = expectation(description: "fullfill test")
}

因为这是一个异步测试,你定义了一个expectation,一旦你侦测到期望的测试结果,你将满足它。

附加下面内容到方法中:

let scheduler = TestScheduler(initialClock: 0)
let observer = scheduler.createObserver(Bool.self)

你创建一个测试调度程序(scheduler),然后使用它创建一个名为observer的测试观察者。你将用你的视图模型的loggedIn属性测试元素发射,因此你可以告诉观察者来监听Bool元素。

现在增加下列代码:

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

下一步,你创建一个 PublishSubject,你将用来测试 AccountStatus值的发射。你传递该主题给 createViewModel(),并最终抓取一个视图模型实例,所有这些都是为测试做准备和建立。

下一步你将订阅在测试下的observable。增加:

let bag = DisposeBag()
let loggedIn = viewModel.loggedIn.asObservable()
  .share()

在这里,您可以获得可共享的连接,并可以采取一些行动。

首先用以下代码订阅 loggedIn到测试观察者:

loggedIn
  .subscribe(observer)
  .addDisposableTo(bag)

然后,为了在完成发送测试值之后结束异步测试,请添加:

loggedIn
  .subscribe(onCompleted: asyncExpect.fulfill)
  .addDisposableTo(bag)

现在所有的订阅在这了,你简单的发射少许测试值。增加:

accountSubject.onNext(.authorized(TestData.account))
accountSubject.onNext(.unavailable)
accountSubject.onCompleted()

最后,检查是否 loggedIn发射了正确的值,增加下面代码来比较记录事件与先前所定义期望的事件列表:

waitForExpectations(timeout: 1.0, handler: { error in
  XCTAssertNil(error, error!.localizedDescription)
  let expectedEvents = [next(0, true), next(0, false), completed(0)]
  XCTAssertEqual(observer.events, expectedEvents)
})

该代码等待异步期望的实现,然后检查记录事件是否是 .next(true), .next(false),和 .completed.的序列。

Note:如果你更愿意,继续并使用RxBlocking重写这个代码。你已经在16章“Testing with RxTest”中学到了如何做

接着,测试用例完成了。高隔离度的视图模型类让你容易的注入模拟对象和仿真输入。阅读测试套件类的其余部分,看看还有什么被测试。如果你想出一些新的测试那应该是很有用的,随意增加吧!

Note:应为在Tweetie项目的视图模型非常好的隔离了应用基础的剩余部分,你不需要运行整个应用来运行测试。窥探iOS Tweetie / AppDelegate.swift,查看代码如何避免在测试过程中创建应用程序的导航和查看控制器。或者,您可以禁用主应用程序进行测试。

现在你有了个全功能的视图模型,也包括在test。是时候使用它了!

Adding an iOS View Controller 366

在本节中,您将编写代码,将视图模型的输出连接到ListTimelineViewController中的视图——这个控制器将在预设的列表中显示组合的用户的推文。

首先,你将工作在iOS版本的Tweetie上。在这个项目的导航中,打开iOS Tweetie/View Controllers/List Timeline。在这里面,你将找到试图控制器和iOS专用的table cell view文件。

打开并浏览下ListTimelineViewController.swift。 ListTimelineViewController类具有视图模型属性和一个导航属性。两个类通过静态方法createWith(navigator:storyboard:viewModel)被注入。

你将增加两个部分启动代码到视图控制器。一个是在 viewDidLoad()中的静态配置,另一个是在 bindUI()中绑定视图模型到UI。

在 viewDidLoad(),调用bindUI()之前增加代码:

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

这将设置列表的名字作为标题并在导航栏右边创建一个新按钮项。

下一步,绑定视图模型。插入下面代码到 bindUI():

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

你订阅右bar项的tap,然后throttle他们来防止任何双击。然后你调用 navigator属性的show(segue:sender:)方法来显示你呈现到屏幕的segue的意图。segue显示人的列表:已经选择Twitter列表成员。

Navigator要么负责呈现请求的屏幕,要么丢弃你的意图,如果它决定执行此操作,那么它可能基于其他参数来决定忽略你希望呈现视图控制器的意图。

Note:通过阅读Navigator类的定义来详细了解类的实现。它包含可导航屏幕所有可能的列表,并且您只能通过提供所有必需的输入参数来调用这些segues。

你也需要创建另一个绑定来在表格视图中显示最新推文。滚动到文件顶部,导入下面的库可以方便的绑定RxRealm结果到表格和集合视图:

import RxRealmDataSources

然后返回到 bindUI()并附加:

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

dataSource是一个表格视图数据源,尤其适合驱动来自Realm集合更改的observable序列的表格视图。在单一行你配置数据源完成:

  1. 你设置模型类型为Tweet
  2. 然后你设置单元格标识符作为 TweetCellView来使用
  3. 最后你提供一个闭包在它显示在屏幕上之前来配置每个单元

你现在能绑定数据资源到视图控制器的表格视图。在最后块的下面增加:

viewModel.tweets
  .bindTo(tableView.rx.realmChanges(dataSource))
  .addDisposableTo(bag)

在这里你绑定 viewModel.tweets到 realmChanges,并提供预处理的数据源。这是您使用动画更改驱动表格视图所需的最低限度。

为这个视图控制器最后的绑定将依据是否用户登录到Twitter来决定在顶部显示或影藏。附加下面代码

viewModel.loggedIn
  .drive(messageView.rx.isHidden)
  .addDisposableTo(bag)

这个绑定开关 messageView.isHidden是基于当前 loggedIn的值的。

这部分展示了为什么绑定是MVVM范式的关键。对于你的视图控制器它仅作为“胶水”代码来服务,这样你就可以轻松地将问题分开。你的视图模型保持了大部分关于当前它运行的平台无关的内容,英文它不导入任何像UIKit或CocoaUId的框架。

运行app并观察所有你闪亮的新视图模型所驱动的绑定:

一旦app完成了JSON请求,消息会在顶部呈现。然后用一个漂亮的动画来抓取推文“蜂拥而来”(pour in)最后,当你点击在右边的bar item时,app将显示用户列表视图控制器:

那就是!在下一节,你将学到跨平台来重用你的视图模型是多么的容易。

Adding a macOS View Controller 369

视图模型不知道任何关于视图或视图控制器的使用。它的意义是,视图模型在需要时是平台独立的。同样的视图模型能容易的提供数据给iOS和macOS的视图控制器。

ListTimelineViewModel恰恰是一个视图模型。它仅仅依赖RxSwift, RxCocoa, and the Realm database。因为这些库是跨平台的,而且视图模型也是跨平台的。

你的工作是切换Xcode项目的macOS目标,然后构造一个视图控制器,镜像你上面构筑的iOS的那个。

从Xcode的scheme选择MacTweetie/My Mac,然后运行项目,看看macOS的起始项目长什么样。

这个app显示了所有包含在预定义的推特账户的列表,但右边窗口显示为空。这个空的视图控制器应该显示tweets timeline。当完成这个app,它应该非常像你为iOS 推特app创建的推文列表。

打开Mac Tweetie/ViewControllers/List Timeline,选择ListTimelineViewController.swift。这个文件名与iOS的视图控制器文件很相似,但是它是在Mac Tweetie文件夹。

通过显示在顶部列表的名字开始,就像在iOS app中所做的一样。增加下面代码到viewDidLoad():

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

现在你能够继续专注在绑定上。如果你浏览过macOS视图控制器的代码,你将注意到像iOS一样,它使用了同样的视图模型和导航类。这是个好消息,因为你已经知道(和爱)ListTimelineViewModel。

视图控制器代码,实际上,与iOS版本几乎相同!该代码相似性是RxSwift的许多好处之一。许多语言的Rx代码也看起来很相似。你将惊奇的发现,你能够阅读并理解RxJava写的Java,或是RxJS写的JavaScript。

与iOS视图控制器类似,向上滚动当前文件,并导入RxRealmDataSources::

import RxRealmDataSources

现在滚动到bindUI()。绑定视图模型的推文到表格视图,增加:

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

这里你用 TweetCellView标识的单元格创建了一个包含Tweet对象的数据源,然后通过调用它上面的 update(with:),在它被重用前,来配置每个单元格。现在创建表格视图绑定。增加下面代码:

let binder = tableView.rx.realmChanges(dataSource)

你通过使用已经初始化的数据源对象,在表格视图行和Realm间创建了一个绑定。

现在你可以简单的绑定视图模型的tweets属性到配置的绑定。增加下面代码:

viewModel.tweets
  .bindTo(binder)
  .addDisposableTo(bag) 

这个绑定让表格视图活了。运行app并查看在右边窗口显示的推文。

这是现实生活还是幻想? 您不必执行任何网络,数据转换或JSON验证?

不,你正在视图控制器上工作,而不是应用程序的其他任何部分。 视图模型负责处理所有内容,因此您唯一需要做的是将数据绑定到UI。

最后一步来打磨下表格视图,因为行被切断(cut off)了。你将使用在 NSTableViewDataSource的方法为行设置制定有高度。因为你正在使用数据源对象,你不能直接在你的表格视图设置 dataSource。替代方法是,你需要告诉数据源对象你正在提供一些你自己的自定义方法。

向上滚动一点,然后增增加下面的代码在你创建binder之前:

dataSource.dataSource = self

现在你需要让 ListTimelineViewController遵循 NSTableViewDataSource,然后增加这个方法来设置表格视图的高度。增加下面代码到文件的底部:

extension ListTimelineViewController: NSTableViewDataSource {
  func tableView(_ tableView: NSTableView, heightOfRow row: Int) -> CGFloat {
    return 68.0
  }
}

你实现了 tableView(_:heightOfRow:),然后返回你想要的行高——68点。如果你喜欢自定义其他部分来呈现你的表格视图,你可以增加其他 NSTableViewDataSource方法,或者设置数据源的 delegate属性,然后增加 NSTableViewDelegate方法到视图控制器。

再一次运行这个app,然后你将看到表格看起来更好:

现在,你对于如何从视图控制器分离你的代码到模型,视图模型和视图有了一个基本的概念。除了简单的应用程序之外,MVVM肯定对MVC有好处,但重要的是要记住,MVVM并不是唯一的选择。

MVVM是与RxSwift一起使用的特别甜蜜的模式,因为Rx使创建绑定成为一项简单的任务。 这导致更简单的代码,更容易阅读和测试。

其他构架样式有不同的优点,其他的库可能更适合这些样式。但是如果您将MVVM + RxSwift视为您可能想要学习的东西,那么肯定会尝试下面的挑战。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容