RxSwift 24 項目實踐

項目實踐

下面是 ViewModel 構(gòu)造時候的最佳實踐(僅供參考), 主要是將 VM 的代碼分成3個類別, 分別是:

  1. Init: 即所有的構(gòu)造方法分為一類, 在它們里面進行各類的依賴注入.
  2. Input: 在這部分包含公共屬性(不一定是 public, 只需要保證 VC 可以正常訪問這些屬性.), 比如 subject, 或是普通屬性, VC 通過它們傳入(input)數(shù)據(jù)到 VM.
  3. Output: 這部分中也是包含的公共屬性(不一定是 public, 只需要保證 VC 可以正常訪問這些屬性.), 但通常都是 Observable. VM 通過它們來向外界提供輸出(Output), 一般來說都是 driver(也是一種特殊的 Observable) 或者是其他 observable. VC 利用這些屬性來驅(qū)動 UI.
VM 中的三個組成部分

一般來說, 項目架構(gòu)是否清晰, 很簡單的衡量方式就是去看 UI, 業(yè)務(wù)邏輯, 以及支撐業(yè)務(wù)邏輯的若干服務(wù)是否擁有良好的封裝.

根據(jù)這樣的標準, 應(yīng)用內(nèi)的元素可以這樣組織:

  • Scene: 用于表示一個 VC 管理的界面, 包含該界面對應(yīng)的 VC 和 View Model, View.
    • View Model: 視圖模型, 包含提供給 VC 使用的業(yè)務(wù)邏輯和數(shù)據(jù).
    • VC: 控制器, 其中僅包含視圖控制邏輯
    • View: 視圖, 即包含的是 UI 的具體實現(xiàn).
  • Service: 服務(wù), 其中包含的是提供給業(yè)務(wù)邏輯代碼使用的各種支撐功能, 比如數(shù)據(jù)庫訪問服務(wù), 網(wǎng)絡(luò) API 訪問服務(wù)等.
  • Models: 模型, 里面包含的是最最基本的數(shù)據(jù)結(jié)構(gòu), VM 和 Service 都是在操作和交換 Model 里面的對象.

在綁定 VC 和對應(yīng)的 VM 時, 有一個好的辦法, 就是像插入兩個可插拔設(shè)備那樣, 給 VM 一個接口, 或是給 VC 一個接口.

例如可以構(gòu)造一個協(xié)議如下所示:

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

associatedtype 指定和協(xié)議相關(guān)的類型名稱占位符. 但該協(xié)議并非是泛型協(xié)議. 在使用的時候只需要在協(xié)議的實現(xiàn)類中指定該類型的實際類型即可:

typealias ViewModelType = Int

這樣所有需要綁定 VM 的 VC 都需要實現(xiàn)這個協(xié)議, 在這里就可以讓持有 vm, 并且在 bindViewModel 方法中對 UI 和 observable 或 action 進行綁定.

而綁定時機需要注意, 一般來說都希望在視圖已經(jīng)建立成功后才會進行綁定. 故在 viewdidload 中去綁定, 而為了讓綁定能夠安全進行, 可以添加一個幫助方法, 在 ViewDidLoad 中去調(diào)用這個方法:

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

這個幫助方法看起來很怪異, 但主要作用就是將 model 賦值給 VC, 并且保證視圖加載完成后再調(diào)用 bindViewModel() 方法.

構(gòu)造 Model 中的基礎(chǔ)對象

比如 Todo List 中的 Item, 如果使用 Realm 存儲的話, 需要像下面這樣構(gòu)造:

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 里面查詢出來的對象都是自動更新的, 即如果數(shù)據(jù)庫中對象變化了, 則之前查詢出來的對象的相應(yīng)屬性也會同樣進行變化.
  • 但上述的特性也有副作用, 若一個對象被從數(shù)據(jù)庫刪除, 則它在內(nèi)存中的所有對象拷貝都將失效. 就是當你去訪問一個被刪除的對象的屬性, 則會出現(xiàn)異常.

構(gòu)造 Task Store 服務(wù)

下面就可以利用 realm 來構(gòu)造對象的存儲服務(wù)了.

構(gòu)造服務(wù)的時候, 最佳實踐是: 構(gòu)造一個 protocol 用于暴露服務(wù)的接口, 構(gòu)造一個服務(wù)的實現(xiàn), 構(gòu)造一個服務(wù)的 mock 實現(xiàn)用于單元測試.

首先構(gòu)造 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>>
}

下面是一個 方法的實現(xiàn)示例:

@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 數(shù)據(jù)庫對象, 并且進行相應(yīng)操作.

提供服務(wù)的實現(xiàn)對象:

 struct TaskService: TaskServiceType {

再看 Scene 如何構(gòu)造

再次強調(diào):

  • Scene 由一個 VC 和一個 VM 構(gòu)成, 相當于一個場景.
  • 其中 VM 包含業(yè)務(wù)邏輯, 在 VM 中實現(xiàn)場景切換, 并且和 VC 實現(xiàn)雙向通信. 但 VM 不知道實際和它溝通的具體 VC, 只是通過接口來交流.
  • VC 只包含視圖控制邏輯, VM 和 View 不能直接通信. 在 VC 中不能進行場景切換, 場景切換是 VM 中的業(yè)務(wù)邏輯驅(qū)動的.

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, 并且各自對應(yīng)有不同的 VM.

下面的代碼演示了 Scene 管理器如何管理 VC 和 VM 以及它們的關(guān)系:

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, 這樣就會導(dǎo)致這樣的方法十分龐大, 故可以對 Scene 進行分層, 即分離為多個 Domain, 然后每個 Domain 對應(yīng)有若干的 Scene, 然后對其中的 Scene 再進行類似管理.

之后就可以使用一個 Scene Coordinator 來管理 Scene 的切換了.

Scene 的切換: 使用 Scene Coordinator

關(guān)于 Scene 的切換, 有很多的方法, 有直接在 VC 進行的, 有使用 route 進行的. 這里使用一種比較簡單的方式, 這樣的方式在若干 app 的構(gòu)建中經(jīng)受住了實踐的檢驗.

下面的圖說明了這樣切換過程:

Scene 切換
  1. Scene A 中的 VM1 實例化 Scene B 關(guān)聯(lián)的 VM2
  2. VM1 調(diào)用 Scene Coordinator 中的方法(比如 transition), 利用它來完成之后的步驟
  3. transition 會調(diào)用之前的 Scene 管理器中的 func viewController() -> UIViewController 方法, 這樣就得到了 VM2 對應(yīng)的 VC
  4. 將對應(yīng) VC 和 VM2 進行綁定
  5. 最后將 VM2 對應(yīng)的 VC 顯示出來.(push, pop, present/modal, and dismiss.)

這樣的架構(gòu)下, 就將 VM 和它們對應(yīng)的 VC 完全隔離開來了.

實現(xiàn) Scene Coordinator

同樣地, 構(gòu)造一個 protocol, 一個協(xié)議實現(xiàn), 一個 mock 實現(xiàn)用于測試.

協(xié)議如下所示:

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 表示沒有任何數(shù)據(jù)返回, 當切換完成的時候輸出 complete.

待續(xù).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容