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.
RxSwift 24 项目实践_第1张图片
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
  @discardableResult
  func delete(task: TaskItem) -> Observable
  @discardableResult
  func update(task: TaskItem, title: String) -> Observable
  @discardableResult
  func toggle(task: TaskItem) -> Observable
  func tasks() -> Observable>
}

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

@discardableResult
func update(task: TaskItem, title: String) -> Observable {
  let result = withRealm("updating title") { realm -> Observable 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 的构建中经受住了实践的检验.

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

RxSwift 24 项目实践_第2张图片
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

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

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

待续.

你可能感兴趣的:(RxSwift 24 项目实践)