RxSwift 24 項目實踐

項目實踐

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

  1. Init: 即所有的構(gòu)造方法分為一類, 在它們里面進(jìn)行各類的依賴注入.
  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ù)這樣的標(biāo)準(zhǔn), 應(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ìn)行綁定.

而綁定時機需要注意, 一般來說都希望在視圖已經(jīng)建立成功后才會進(jìn)行綁定. 故在 viewdidload 中去綁定, 而為了讓綁定能夠安全進(jìn)行, 可以添加一個幫助方法, 在 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 的對象不能跨線程使用, 如果要在其他線程使用某個對象, 需要重新進(jìn)行查詢, 或者是使用 realm 提供的 ThreadSafeReference.
  • 從 realm 里面查詢出來的對象都是自動更新的, 即如果數(shù)據(jù)庫中對象變化了, 則之前查詢出來的對象的相應(yīng)屬性也會同樣進(jìn)行變化.
  • 但上述的特性也有副作用, 若一個對象被從數(shù)據(jù)庫刪除, 則它在內(nèi)存中的所有對象拷貝都將失效. 就是當(dāng)你去訪問一個被刪除的對象的屬性, 則會出現(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 是一個幫助方法, 用于獲取當(dāng)前的 realm 數(shù)據(jù)庫對象, 并且進(jìn)行相應(yīng)操作.

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

 struct TaskService: TaskServiceType {

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

再次強調(diào):

  • Scene 由一個 VC 和一個 VM 構(gòu)成, 相當(dāng)于一個場景.
  • 其中 VM 包含業(yè)務(wù)邏輯, 在 VM 中實現(xiàn)場景切換, 并且和 VC 實現(xiàn)雙向通信. 但 VM 不知道實際和它溝通的具體 VC, 只是通過接口來交流.
  • VC 只包含視圖控制邏輯, VM 和 View 不能直接通信. 在 VC 中不能進(jìn)行場景切換, 場景切換是 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 進(jìn)行分層, 即分離為多個 Domain, 然后每個 Domain 對應(yīng)有若干的 Scene, 然后對其中的 Scene 再進(jìn)行類似管理.

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

Scene 的切換: 使用 Scene Coordinator

關(guān)于 Scene 的切換, 有很多的方法, 有直接在 VC 進(jìn)行的, 有使用 route 進(jìn)行的. 這里使用一種比較簡單的方式, 這樣的方式在若干 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 進(jìn)行綁定
  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ù)返回, 當(dāng)切換完成的時候輸出 complete.

待續(xù).

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鲫咽,一起剝皮案震驚了整個濱河市窘游,隨后出現(xiàn)的幾起案子松忍,更是在濱河造成了極大的恐慌,老刑警劉巖之剧,帶你破解...
    沈念sama閱讀 219,490評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件佣耐,死亡現(xiàn)場離奇詭異嘀倒,居然都是意外死亡矫夯,警方通過查閱死者的電腦和手機鸽疾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,581評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來训貌,“玉大人制肮,你說我怎么就攤上這事⊥” “怎么了弄企?”我有些...
    開封第一講書人閱讀 165,830評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長区拳。 經(jīng)常有香客問我拘领,道長,這世上最難降的妖魔是什么樱调? 我笑而不...
    開封第一講書人閱讀 58,957評論 1 295
  • 正文 為了忘掉前任约素,我火速辦了婚禮,結(jié)果婚禮上笆凌,老公的妹妹穿的比我還像新娘圣猎。我一直安慰自己,他們只是感情好乞而,可當(dāng)我...
    茶點故事閱讀 67,974評論 6 393
  • 文/花漫 我一把揭開白布送悔。 她就那樣靜靜地躺著,像睡著了一般爪模。 火紅的嫁衣襯著肌膚如雪欠啤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,754評論 1 307
  • 那天屋灌,我揣著相機與錄音洁段,去河邊找鬼。 笑死共郭,一個胖子當(dāng)著我的面吹牛祠丝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播除嘹,決...
    沈念sama閱讀 40,464評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼写半,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了尉咕?” 一聲冷哼從身側(cè)響起叠蝇,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎龙考,沒想到半個月后蟆肆,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體矾睦,經(jīng)...
    沈念sama閱讀 45,847評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,995評論 3 338
  • 正文 我和宋清朗相戀三年炎功,在試婚紗的時候發(fā)現(xiàn)自己被綠了枚冗。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,137評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡蛇损,死狀恐怖赁温,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情淤齐,我是刑警寧澤股囊,帶...
    沈念sama閱讀 35,819評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站更啄,受9級特大地震影響稚疹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜祭务,卻給世界環(huán)境...
    茶點故事閱讀 41,482評論 3 331
  • 文/蒙蒙 一内狗、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧义锥,春花似錦柳沙、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,023評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至柱恤,卻和暖如春数初,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背膨更。 一陣腳步聲響...
    開封第一講書人閱讀 33,149評論 1 272
  • 我被黑心中介騙來泰國打工妙真, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缴允,地道東北人荚守。 一個月前我還...
    沈念sama閱讀 48,409評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像练般,于是被迫代替她去往敵國和親矗漾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,086評論 2 355

推薦閱讀更多精彩內(nèi)容