項目實踐
下面是 ViewModel 構(gòu)造時候的最佳實踐(僅供參考), 主要是將 VM 的代碼分成3個類別, 分別是:
- Init: 即所有的構(gòu)造方法分為一類, 在它們里面進(jìn)行各類的依賴注入.
- Input: 在這部分包含公共屬性(不一定是 public, 只需要保證 VC 可以正常訪問這些屬性.), 比如 subject, 或是普通屬性, VC 通過它們傳入(input)數(shù)據(jù)到 VM.
- Output: 這部分中也是包含的公共屬性(不一定是 public, 只需要保證 VC 可以正常訪問這些屬性.), 但通常都是 Observable. VM 通過它們來向外界提供輸出(Output), 一般來說都是 driver(也是一種特殊的 Observable) 或者是其他 observable. VC 利用這些屬性來驅(qū)動 UI.
一般來說, 項目架構(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 A 中的 VM1 實例化 Scene B 關(guān)聯(lián)的 VM2
- VM1 調(diào)用 Scene Coordinator 中的方法(比如 transition), 利用它來完成之后的步驟
- transition 會調(diào)用之前的 Scene 管理器中的
func viewController() -> UIViewController
方法, 這樣就得到了 VM2 對應(yīng)的 VC - 將對應(yīng) VC 和 VM2 進(jìn)行綁定
- 最后將 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ù).