美文网首页RxSwift学习
RxSwift 24 项目实践

RxSwift 24 项目实践

作者: 貘鸣 | 来源:发表于2017-10-09 17:29 被阅读62次

    项目实践

    下面是 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.

    待续.

    相关文章

      网友评论

        本文标题:RxSwift 24 项目实践

        本文链接:https://www.haomeiwen.com/subject/fjfpyxtx.html