RxSwift 24 项目实践

项目实践

下面是 ViewModel 构造时候的最佳实践(仅供参考), 主要是将 VM 的代码分成3个类别, 分别是:

  1. Init: 即所有的构造方法分为一类, 在它们里面进行各类的依赖注入.
  2. Input: 在这部分包含公共属性(不一定是 public, 只需要保证 VC 可以正常访问这些属性.), 比如 subject, 或是普通属性, VC 通过它们传入(input)数据到 VM.
  3. Output: 这部分中也是包含的公共属性(不一定是 public, 只需要保证 VC 可以正常访问这些属性.), 但通常都是 Observable. VM 通过它们来向外界提供输出(Output), 一般来说都是 driver(也是一种特殊的 Observable) 或者是其他 observable. VC 利用这些属性来驱动 UI.
VM 中的三个组成部分

一般来说, 项目架构是否清晰, 很简单的衡量方式就是去看 UI, 业务逻辑, 以及支撑业务逻辑的若干服务是否拥有良好的封装.

根据这样的标准, 应用内的元素可以这样组织:

  • Scene: 用于表示一个 VC 管理的界面, 包含该界面对应的 VC 和 View Model, View.
    • View Model: 视图模型, 包含提供给 VC 使用的业务逻辑和数据.
    • VC: 控制器, 其中仅包含视图控制逻辑
    • View: 视图, 即包含的是 UI 的具体实现.
  • Service: 服务, 其中包含的是提供给业务逻辑代码使用的各种支撑功能, 比如数据库访问服务, 网络 API 访问服务等.
  • Models: 模型, 里面包含的是最最基本的数据结构, VM 和 Service 都是在操作和交换 Model 里面的对象.

在绑定 VC 和对应的 VM 时, 有一个好的办法, 就是像插入两个可插拔设备那样, 给 VM 一个接口, 或是给 VC 一个接口.

例如可以构造一个协议如下所示:

protocol BindableType {
  associatedtype ViewModelType
  var viewModel: ViewModelType! { get set }
  func bindViewModel()
}

associatedtype 指定和协议相关的类型名称占位符. 但该协议并非是泛型协议. 在使用的时候只需要在协议的实现类中指定该类型的实际类型即可:

typealias ViewModelType = Int

这样所有需要绑定 VM 的 VC 都需要实现这个协议, 在这里就可以让持有 vm, 并且在 bindViewModel 方法中对 UI 和 observable 或 action 进行绑定.

而绑定时机需要注意, 一般来说都希望在视图已经建立成功后才会进行绑定. 故在 viewdidload 中去绑定, 而为了让绑定能够安全进行, 可以添加一个帮助方法, 在 ViewDidLoad 中去调用这个方法:

extension BindableType where Self: UIViewController {
  mutating func bindViewModel(to model: Self.ViewModelType) {
    viewModel = model
    loadViewIfNeeded()
    bindViewModel()
  } 
}

这个帮助方法看起来很怪异, 但主要作用就是将 model 赋值给 VC, 并且保证视图加载完成后再调用 bindViewModel() 方法.

构造 Model 中的基础对象

比如 Todo List 中的 Item, 如果使用 Realm 存储的话, 需要像下面这样构造:

class TaskItem: Object {
    dynamic var uid: Int = 0
    dynamic var title: String = ""
    dynamic var added: Date = Date()
    dynamic var checked: Date? = nil
    override class func primaryKey() -> String? {
        return "uid"
    }
}

在使用 Realm 的时候需要注意如下事项:

  • realm 的对象不能跨线程使用, 如果要在其他线程使用某个对象, 需要重新进行查询, 或者是使用 realm 提供的 ThreadSafeReference.
  • 从 realm 里面查询出来的对象都是自动更新的, 即如果数据库中对象变化了, 则之前查询出来的对象的相应属性也会同样进行变化.
  • 但上述的特性也有副作用, 若一个对象被从数据库删除, 则它在内存中的所有对象拷贝都将失效. 就是当你去访问一个被删除的对象的属性, 则会出现异常.

构造 Task Store 服务

下面就可以利用 realm 来构造对象的存储服务了.

构造服务的时候, 最佳实践是: 构造一个 protocol 用于暴露服务的接口, 构造一个服务的实现, 构造一个服务的 mock 实现用于单元测试.

首先构造 protocol:

protocol TaskServiceType {
  @discardableResult
  func createTask(title: String) -> Observable<TaskItem>
  @discardableResult
  func delete(task: TaskItem) -> Observable<Void>
  @discardableResult
  func update(task: TaskItem, title: String) -> Observable<TaskItem>
  @discardableResult
  func toggle(task: TaskItem) -> Observable<TaskItem>
  func tasks() -> Observable<Results<TaskItem>>
}

下面是一个 方法的实现示例:

@discardableResult
func update(task: TaskItem, title: String) -> Observable<TaskItem> {
  let result = withRealm("updating title") { realm -> Observable<TaskItem> in
    try realm.write {
      task.title = title
    }
    return .just(task)
  }
  return result ?? .error(TaskServiceError.updateFailed(task))
}

其中 withRealm 是一个帮助方法, 用于获取当前的 realm 数据库对象, 并且进行相应操作.

提供服务的实现对象:

 struct TaskService: TaskServiceType {

再看 Scene 如何构造

再次强调:

  • Scene 由一个 VC 和一个 VM 构成, 相当于一个场景.
  • 其中 VM 包含业务逻辑, 在 VM 中实现场景切换, 并且和 VC 实现双向通信. 但 VM 不知道实际和它沟通的具体 VC, 只是通过接口来交流.
  • VC 只包含视图控制逻辑, VM 和 View 不能直接通信. 在 VC 中不能进行场景切换, 场景切换是 VM 中的业务逻辑驱动的.

At this stage, a view model can instantiate another view model and assign it to its scene, ready for transition.

新建一个类似 Scene 管理器的实体(Scene 枚举), 添加如下代码:

enum Scene {
    case tasks(TasksViewModel)
    case editTask(EditTaskViewModel)
}

表明 APP 里面有两个 Scene, tasks 和 editTask, 并且各自对应有不同的 VM.

下面的代码演示了 Scene 管理器如何管理 VC 和 VM 以及它们的关系:

extension Scene {
  func viewController() -> UIViewController {
    let storyboard = UIStoryboard(name: "Main", bundle: nil)
    switch self {
    case .tasks(let viewModel):
      let nc = storyboard.instantiateViewController(withIdentifier:
"Tasks") as! UINavigationController
      var vc = nc.viewControllers.first as! TasksViewController
      vc.bindViewModel(to: viewModel)
      return nc
    case .editTask(let viewModel):
      let nc = storyboard.instantiateViewController(withIdentifier:
"EditTask") as! UINavigationController
      var vc = nc.viewControllers.first as! EditTaskViewController
      vc.bindViewModel(to: viewModel)
      return nc
  }
 }
}

不过在大型项目中可能有若干的 Scene, 这样就会导致这样的方法十分庞大, 故可以对 Scene 进行分层, 即分离为多个 Domain, 然后每个 Domain 对应有若干的 Scene, 然后对其中的 Scene 再进行类似管理.

之后就可以使用一个 Scene Coordinator 来管理 Scene 的切换了.

Scene 的切换: 使用 Scene Coordinator

关于 Scene 的切换, 有很多的方法, 有直接在 VC 进行的, 有使用 route 进行的. 这里使用一种比较简单的方式, 这样的方式在若干 app 的构建中经受住了实践的检验.

下面的图说明了这样切换过程:

Scene 切换
  1. Scene A 中的 VM1 实例化 Scene B 关联的 VM2
  2. VM1 调用 Scene Coordinator 中的方法(比如 transition), 利用它来完成之后的步骤
  3. transition 会调用之前的 Scene 管理器中的 func viewController() -> UIViewController 方法, 这样就得到了 VM2 对应的 VC
  4. 将对应 VC 和 VM2 进行绑定
  5. 最后将 VM2 对应的 VC 显示出来.(push, pop, present/modal, and dismiss.)

这样的架构下, 就将 VM 和它们对应的 VC 完全隔离开来了.

实现 Scene Coordinator

同样地, 构造一个 protocol, 一个协议实现, 一个 mock 实现用于测试.

协议如下所示:

protocol SceneCoordinatorType {

  init(window: UIWindow)

  @discardableResult
  func transition(to scene: Scene, type: SceneTransitionType) -> Observable<Void>

  @discardableResult
  func pop(animated: Bool) -> Observable<Void>
}

其中的 SceneTransitionType 就可以指定是何种切换方式, 比如 push 或者 present, dismiss 等.
返回值中的 Observable 表示没有任何数据返回, 当切换完成的时候输出 complete.

待续.

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

推荐阅读更多精彩内容