RxSwift — ViewModel done right

RxSwift — ViewModel done right


The blueprint you’ve been looking for.




Action is a great and small module used to abstract the concept of… an action in RxSwift. Check it out, and use it everywhere it fits


NB: goes hand-in-hand with ViewController done right and Coordinator pattern done right; they are part of the same whole.

注意:同 ViewController done rightCoordinator pattern done right携手共进吧,它们是这个系列的一部分。

In a nutshell 简而言之

There are a lot of different ways one might structure a ViewModel, the same way one might structure an investment portfolio; there is no “best” way but there definitely are “better” ways. Let’s focus on what seemed to be the safest and most polyvalent option so far.


Some of the rules/patterns presented here might either seem weird or repetitive or overkill at first glance; bear in mind they:

这里提出的一些规则/范式可能看起来很奇怪或重复或者过度凶猛; 记住他们:

  1. provide a consistent structure and API across ViewModels which you can confidently rely on today and 2 months from now


  2. are insurance against your future dumb self poking around the code trying to agile-esquely rush a new feature and smearing the code base in the process (it will of course never be refactored even though you knew and said it was just a “quick-and-dirty” hotfix that would be reintegrated in the blablabla)

  3. **are of tremendous help in bringing existing and future colleagues up to speed **on MVVM and RxSwift as well as enabling them to review your code more efficiently

What should never be in a ViewModel

Whenever you make an exception to these, think long and hard before doing so.

  1. A ViewModel should NEVER import UIKit, though extremely rare exceptions can be made when working with UIImage or another UI Type. In this case, then restrict to the bare minimum such as import UIKit.UIImage

    ViewModel不应该使用import UIKit,即使在使用UIImage或其他UI类型时极少能够做到。在这种情况下,应该最小限度的导入,如:import UIKit.UIImage

  2. A ViewModel should never implement a DisposeBag, except for subscriptions that should be bound to the ViewModel's lifecycle (if any)


  3. Only use Variables where absolutely necessary: it's often only about re-writing a smarter .scan


Caveat: let’s be honest, your ViewModel will almost always have life cycles if you are doing more than the latest FartApp. Points 3 is more of a reminder to always hold back on creating Variables, the use cases of which are almost always tied to point 2.

警惕:老实说,如果你做的不只是最新的FartApp,你的ViewModel将几乎总是保持生命周期。 点3更多的是提醒人们总是坚持创建Variables,这里的使用情况几乎总是与点2相关。

Recipe for a robust ViewModel 健壮的ViewModel的秘诀

A ViewModel has only one mission: to transform inputs received from either dependency injection or its ViewController and expose outputs for its ViewController to bind to.


Let’s take a look at the basic structure and break it down from top to bottom:


import RxSwift
import Action

protocol MyViewModelInputsType {
    // Inputs headers

protocol MyViewModelOutputsType {
    // Outputs headers

protocol MyViewModelActionsType {
    // Actions headers

protocol MyViewModelType: class {
    var inputs: MyViewModelInputsType { get }
    var outputs: MyViewModelOutputsType { get }
    var actions: MyViewModelActionsType { get }

final class MyViewModel: MyViewModelType {
    var inputs: MyViewModelInputsType { return self }
    var outputs: MyViewModelOutputsType { return self }
    var actions: MyViewModelActionsType { return self }
    // Setup
    private let myViewModelService: MyViewModelServiceType
    private let coordinator: SceneCoordinatorType
    // Inputs
    // Outputs
    // ViewModel Life Cycle
    init(service: MyViewModelServiceType, coordinator: SceneCoordinatorType) {
        // Setup
        self.myViewModelService = service
        self.coordinator = coordinator

        // Inputs
        // Outputs
        // ViewModel Life Cycle

    // Actions

extension MyViewModel: MyViewModelInputsType, MyViewModelOutputsType, MyViewModelActionsType { }

The top 3 protocols define the purpose of the ViewModel. They follow simple rules:


  • **Inputs always are of type **PublishSubject<T>: somebody needs to push stuff with .onNext to the ViewModel which means inputs must be observers (duh). Sometimes, because you are smart and use Action , they might be of type InputSubject<T> which is basically the same except the latter cannot error out or complete

    Inputs总是使用 PublishSubject<T>类型:有人需要用.onNext将东西推送到ViewModel,这意味着输入必须是observers(duh)。 有时,因为你机智的使用了Action,它们可能是InputSubject<T>类型,但它们基本上是一样的,除了后者不能输出error或complete

  • **Outputs always are of type **Observable<T>: somebody will observe the ViewModel (otherwise, well you don’t need one in the first place) and the only thing they need is a read-only stream to look at in order to react accordingly

    Outputs总是使用Observable<T>:有人会观察ViewModel(或者说,well you don’t need one in the first place),并且他们唯一需要的是只读流,以便相应地进行响应

  • **Actions always are of type **Action<T, U> or CocoaAction (which is just a typealias for Action<Void, Void>): you don’t really have a choice though, just don’t put anything else in there that’s not from the Action module

    Actions总是Action<T, U>CocoaAction(它是 Action<Void, Void>的别名) 类型:你真的没有选择,不要把任何不是来自Action模块的东西放在那里。

The MyViewModelType protocol simply enforces the need for the same three variables to be created every time, which basically are your API to the ViewModel.

MyViewModelType 协议强制需要每次创建相同的三个变量,这些变量基本上是就你ViewModel的API。

These variables are implemented as computed and return self which is why down at the bottom the ViewModel needs to conform to their protocol specs. They are all the way down just to visually de-clutter the code.

这些变量用计算属性实现并返回 self ,这就是为什么在底部的ViewModel需要符合他们的协议规范。 他们放在最下来,只是为了避免代码的视觉混乱。

Now that the ViewModel skeleton is in place, let’s get to the meat:


  1. Everything happens in the init: nothing gets initialized outside of it, all bindings are set up


  2. There always is a service for your ViewModel: it represents the “building blocks” helper struct that it can consume/assemble from to produce outputs [see Services done right]

    您的ViewModel总是有一个service:它代表了可以消耗/组合从而产生输出“构建块”的辅助 struct [参见Services done right]

  3. There (almost) always is a reference to the app coordinator: when the ViewModel is bound to by a controller. You do not need a reference to it when it is bound to by a view, such as a UICollectionViewCell

    那里(几乎)总有对应用程序协调器的引用:当ViewModel被控制器绑定时。 当绑定到一个视图时,例如UICollectionViewCell,您不需要引用它。

  4. There is nothing more than actions from the Action module below init: every “func” you imagine to produce output observables either sits in the service struct as a helper or can and should be abstracted as an action

    除了init之下的Action模块之外没有更多的actions,你设想用来产生输出的observables,或者位于service struct 作为一个helper,或者可以并且应该被抽象为一个动作。

Real life example

Send stuff to a sending list with a database upload and present a new scene.

Credits to Shai Mishali for inspiring the structure.

  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 119,054评论 1 241
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 51,980评论 1 200
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 74,402评论 0 167
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 36,473评论 0 127
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 43,290评论 1 206
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 35,928评论 1 126
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 27,913评论 2 207
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 27,039评论 0 120
  • 想象着我的养父在大火中拼命挣扎,窒息,最后皮肤化为焦炭。我心中就已经是抑制不住地欢快,这就叫做以其人之道,还治其人...
    爱写小说的胖达阅读 26,005评论 5 173
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 30,108评论 0 178
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 27,307评论 1 170
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 28,560评论 1 178
  • 白月光回国,霸总把我这个替身辞退。还一脸阴沉的警告我。[不要出现在思思面前, 不然我有一百种方法让你生不如死。]我...
    爱写小说的胖达阅读 22,865评论 0 25
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 25,485评论 2 165
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 29,341评论 3 173
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 24,325评论 0 4
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 24,351评论 0 113
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 30,542评论 2 189
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 30,958评论 2 188


  • **2014真题Directions:Read the following text. Choose the be...
    又是夜半惊坐起阅读 8,151评论 0 23
  • 践行第十天 关于社会上的大事,我不太关注,因为谁做了皇帝都与我无关,无论是谁,我都依然是自己现在的生活,其他不好的...
    李李青青阅读 163评论 0 1
  • 推荐书籍 人的宗教 本书不是宗教史,而是环绕宗教引发的价值进行论述,人生下来必须有信仰,不一定非得是宗教,但是...
    胖胖胖蓝胖子阅读 93评论 0 1