RxSwift_v1.0筆記——24 Building a Complete RxSwift App
通過本書精偿,你學(xué)習(xí)到了RxSwift的許多方面创南。響應(yīng)式編程是一個很深的主題;它的采用在多數(shù)情況下會與你已經(jīng)成熟使用的構(gòu)建有很大差異惫企。在RxSwift中你構(gòu)建事件和數(shù)據(jù)流的方式對正確的行為來說是重要的,它也保證了產(chǎn)品未來的發(fā)展。
你將構(gòu)建一個小的RxSwift應(yīng)用來結(jié)束本書彼念。這個目標(biāo)不是“不惜任何代價”使用Rx挪圾,而是使設(shè)計決策引導(dǎo)一個具有穩(wěn)定,可預(yù)測和模塊化行為的干凈的架構(gòu)逐沙。這個應(yīng)用設(shè)計比較簡單哲思,清晰的呈現(xiàn)了你能夠用來構(gòu)建你自己的應(yīng)用的思想。
本章是關(guān)于RxSwift的吩案,也是適合你需要的一個好的構(gòu)架棚赔。RxSwift是一個偉大的工具,它幫助你的應(yīng)用運行起來像一個精心調(diào)校(well-tuned)的引擎徘郭,但它對于思考和設(shè)計應(yīng)用程序架構(gòu)來說不是多余的靠益。
Introducing QuickTodo 376
作為“hello world”程序的現(xiàn)代版,“To-Do”應(yīng)用程序是展現(xiàn)Rx應(yīng)用程序內(nèi)部結(jié)構(gòu)的理想選擇残揉。
在上一章中胧后,你了解到有關(guān)MVVM以及與響應(yīng)式編程相匹配的情況。 你將使用MVVM構(gòu)建QuickTodo應(yīng)用程序抱环,并了解如何隔離代碼的數(shù)據(jù)處理部分并使其完全獨立壳快。
Architecting the application 376
一個你應(yīng)用的尤其重要的目標(biāo),是完成用戶界面與應(yīng)用的業(yè)務(wù)邏輯的分離镇草,以及應(yīng)用程序包含的服務(wù)來幫助業(yè)務(wù)邏輯運行眶痰。為此(To that end),你真的需要一個清晰的模型梯啤,其中每個組件都被明確定義竖伯。
首先,讓我們介紹一些你將實現(xiàn)的構(gòu)建的一些術(shù)語:
- Scene:指由視圖控制器管理的屏幕条辟。它可以是常規(guī)屏幕黔夭,或模態(tài)對話框(modal dialog)。它由一個視圖控制器和一個視圖模型組成羽嫡。
- View model:定義業(yè)務(wù)邏輯和數(shù)據(jù)給視圖控制器使用本姥,來呈現(xiàn)一個特定的場景。
- Service:一個功能性的邏輯組提供給在應(yīng)用中的任何場景杭棵。例如婚惫,數(shù)據(jù)庫存儲能夠被抽象為一個服務(wù)。同樣的魂爪,網(wǎng)絡(luò)API請求能夠被分組到網(wǎng)絡(luò)服務(wù)先舷。
- Model:存儲在應(yīng)用中大部分的基礎(chǔ)數(shù)據(jù)。視圖模型和服務(wù)都操作和交換模型滓侍。
在上一章“MVVM with RxSwift”中你學(xué)習(xí)了視圖模型蒋川。Services是一個新的概念并且也適合與響應(yīng)式編程。他們的目的是竟可能的使用Observable和Observer暴露數(shù)據(jù)和功能撩笆,以便創(chuàng)建一個全局模型捺球,其中的組件竟可能以響應(yīng)方式的連接在一起缸浦。
對于你的QuickTodo應(yīng)用,需求相當(dāng)適用氮兵。正確構(gòu)建裂逐,將為你未來的發(fā)展奠定堅實的基礎(chǔ)。它也是一個你可以重用于其他app的構(gòu)架泣栈。
你需要了解的基礎(chǔ)項:
- 一個TaskItem model卜高,它表示一個獨立任務(wù)。
- 一個TaskService service南片,它提供了任務(wù)創(chuàng)建掺涛、更新、刪除铃绒、存儲和搜索鸽照。
- 一個storage medium;你將使用一個Realm數(shù)據(jù)庫和RxRealm颠悬。
- 一個系列的創(chuàng)建和搜索任務(wù)的scenes列表矮燎。每個scene分離到一個視圖模型和一個視圖控制器。
- 一個scene coordinator對象來管理場景的導(dǎo)航和顯示赔癌。
正如你上章所學(xué)的诞外,視圖模型暴露了業(yè)務(wù)邏輯和數(shù)據(jù)模型給視圖控制器。接下來你將為每個視圖模型創(chuàng)建簡單的規(guī)則:
- 暴露數(shù)據(jù)作為observable序列灾票。這保證了一旦連接到用戶界面就自動更新峡谊。
- 使用 Action樣式將暴露的所有視圖模型的動作連接到UI。
- 任何可公開訪問的模型或數(shù)據(jù)刊苍,不會作為observable序列暴露既们,且都是不可變的。
- 從一個場景轉(zhuǎn)換到另個場景是業(yè)務(wù)邏輯的一部分正什。每個視圖模型初始化這個轉(zhuǎn)換并準(zhǔn)備下一個場景的視圖模型啥纸,而不需要指定關(guān)于視圖模型的任何事。
從當(dāng)前的視圖控制器完全隔離視圖模型婴氮,包含觸發(fā)到其他場景的轉(zhuǎn)換的解決方案斯棒,本章稍后將會介紹。
Note:數(shù)據(jù)的不變性保證了對由UI觸發(fā)的更新的完全控制主经。嚴(yán)格遵循以上規(guī)則也保證了每個代碼塊最好的可測試性荣暮。
前章展示了如何在didSet的幫助下,使用可變屬性來更新底層模型罩驻。本章將通過完全刪除可變性并僅暴露Actions穗酥,來更深入的采用此觀念。
Bindable view controllers 378
你將從視圖控制器開始。在某些時候迷扇,你需要連接百揭,或綁定視圖控制器到與它相關(guān)的視圖模型。做這個的一種方式是你的控制器采用一個特定的協(xié)議:BindableType蜓席。
Note:本章的起始項目包含了相當(dāng)多的代碼。當(dāng)你第一次用Xcode打開項目時课锌,將不能編譯成功厨内。在你構(gòu)筑并運行前,你需要增加一些關(guān)鍵的點渺贤。
打開BindableType.swift 然后增加基本的協(xié)議:
protocol BindableType {
associatedtype ViewModelType
var viewModel: ViewModelType! { get set }
func bindViewModel()
}
每個視圖控制器遵循BindableType協(xié)議雏胃,它聲明了一個viewModel變量并且,一旦viewModel變量被分配就調(diào)用提供的一個bindViewModel()函數(shù)志鞍。這個函數(shù)將連接UI元素到在視圖模型中的observables和actions瞭亮。
Binding at the right moment 379
綁定有一個特殊的地方需要注意。你希望盡快將viewModel變量分配到你的視圖控制器固棚,但是bindViewModel()必須在視圖加載之后調(diào)用统翩。
這是因為你的bindViewModel()函數(shù)通常會連接需要曾現(xiàn)的UI元素。為此此洲,你將使用一個小的幫助函數(shù)厂汗,在實例化每個視圖控制器之后來調(diào)用它。增加這個到BindableType.swift:
extension BindableType where Self: UIViewController {
mutating func bindViewModel(to model: Self.ViewModelType) {
viewModel = model
loadViewIfNeeded()
bindViewModel()
}
}
這樣呜师,在你的視圖控制器調(diào)用 viewDidLoad()時娶桦,確保了 viewModel已經(jīng)被分配。 由于viewDidLoad()是設(shè)置視圖控制器標(biāo)題以便平滑推送導(dǎo)航標(biāo)題動畫的最佳時間汁汗,你可能需要訪問視圖模型以準(zhǔn)備標(biāo)題衷畦,加載視圖控制器,如果需要知牌,這樣是最有效的方案祈争。
Task model 379
你的任務(wù)模型是簡單的且來源于Realm的基本對象。任務(wù)定義為有一個標(biāo)題(任務(wù)內(nèi)容)送爸,一個創(chuàng)建日期和一個檢查日期铛嘱。日期被用來在任務(wù)列表中對任務(wù)排序。如果你不熟悉Realm袭厂,請查看他們的文檔:https://realm.io/docs/swift/latest/墨吓。
填充TaskItem.swift如下:
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數(shù)據(jù)庫的特定對象,有兩個你需要詳細(xì)知道的細(xì)節(jié)是:
- Object不能跨線程纹磺。如果你需要一個在不同的線程的對象帖烘,要么重新查詢,要么使用Realm的 ThreadSafeReference橄杨。
- Objects是自動更新的秘症。如果你改變了數(shù)據(jù)庫照卦,它會立即反映到來至數(shù)據(jù)庫的任何被查詢的活動對象的屬性中。稍后你將看到它是如何使用的乡摹。
- 因此役耕,刪除對象會使所有現(xiàn)有副本無效。如果你訪問了一個被刪除的查詢對象的屬性聪廉,將會拋出異常瞬痘。
上面的第二點有副作用,你將在本章后面更詳細(xì)地研究綁定任務(wù)單元格板熊。
Tasks service 380
Tasks service的責(zé)任是創(chuàng)建框全、更新和抓取來至存儲的任務(wù)項。作為一個可靠的開發(fā)者干签,你將使用協(xié)議來定義你的服務(wù)公共接口津辩,然后寫一個運行時的實現(xiàn)并為測試模擬實現(xiàn)。
首先容劳,創(chuàng)建協(xié)議喘沿。這是你將暴露給用戶的服務(wù)。
打開TaskServiceType.swift鸭蛙,增加協(xié)議的定義:
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>>
}
這是一個基本的接口摹恨,提供了基礎(chǔ)服務(wù)來創(chuàng)建,刪除更新和查詢?nèi)蝿?wù)娶视。沒什么有趣的晒哄。大部分重要的細(xì)節(jié)是服務(wù)暴露了作為observable序列的數(shù)據(jù)。即使是創(chuàng)建肪获,刪除寝凌,更新和開關(guān)任務(wù)的函數(shù)也返回一個你可以訂閱的observable。
它的核心概念是孝赫,通過observables的成功完成较木,來傳輸任何操作的失敗或成功。另外青柄,在Actions中你能夠使用返回的observable作為返回值伐债。你將在本章稍后看到一些例子。
例如致开,打開TaskService.swift峰锁,你將看到 update(task:title:)是這樣的:
@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(:action :)是一個內(nèi)部封裝,可以獲取當(dāng)前的Realm數(shù)據(jù)庫并對其進(jìn)行操作双戳。如果拋出錯誤虹蒋,withRealm(:action:)將始終返回nil。 這是一個很好的機(jī)會返回一個錯誤,可以將錯誤信號發(fā)送給調(diào)用者魄衅。
你不需要從頭到尾完成tasks service的實現(xiàn)峭竣,但是你應(yīng)該花點時間瀏覽下TaskService.swift中的代碼。
你做的最后一件事是添加TaskServiceType晃虫,現(xiàn)在打開TaskService.swift并使其符合該協(xié)議:
struct TaskService: TaskServiceType {
你已經(jīng)完成了tasks service皆撩!你的視圖模型將接收TaskServiceType對象,不論是真實的還是在測試期間模擬的傲茄,都應(yīng)該能夠工作毅访。
Scenes 381
你通過以上了解到,在本章的架構(gòu)中盘榨,場景是由視圖控制器和視圖模型管理的“屏幕”構(gòu)成的邏輯展示單元。場景的規(guī)則有:
- 視圖模型處理業(yè)務(wù)邏輯蟆融。這包括開始轉(zhuǎn)換到另一個“場景”
- 視圖模型對于實際的視圖控制器和用于表示場景的視圖一無所知草巡。
- 視圖控制器不應(yīng)該發(fā)起到另一個場景的轉(zhuǎn)換;這是運行在視圖模型中的業(yè)務(wù)邏輯的責(zé)任型酥。
考慮到這一點山憨,你可以制定(lay down)一個模型,應(yīng)用場景作為case列在Scene枚舉中弥喉,每種case都有將場景視圖模型作為其相關(guān)數(shù)據(jù)郁竟。
Note:這與你在上一章中導(dǎo)航類中所做的很相似,但是使用Scene由境,導(dǎo)航會變得更加靈活棚亩。
打開Scene.swift。你將在我們的app中定義兩個我們需要的場景虏杰,tasks和editTask讥蟆。增加:
enum Scene {
case tasks(TasksViewModel)
case editTask(EditTaskViewModel)
}
在這個階段,視圖模型可以實例化另一個視圖模型并將其分配給其場景纺阔,準(zhǔn)備轉(zhuǎn)換瘸彤。 你也可以為視圖模型實現(xiàn)基本的約定,盡可能不要依賴于UIKit笛钝。
現(xiàn)在即將添加的Scene枚舉的擴(kuò)展质况,會暴露一個函數(shù),該函數(shù)是實例化場景視圖控制器的唯一位置玻靡。 該函數(shù)將知道如何從每個場景的資源中拉取視圖控制器结榄。
打開Scene+ViewController.swift,增加這個函數(shù):
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
}
}
}
這個代碼實例化了相關(guān)的視圖控制器并立即綁定到它的視圖模型啃奴,它是來至數(shù)據(jù)相關(guān)聯(lián)的每個枚舉case潭陪。
Note:當(dāng)在你的app中有很多場景時,這個函數(shù)將變得很長。不要猶豫依溯,分離它到多個部分以便清晰和可維護(hù)老厌。在具有多個域的大型應(yīng)用程序中,您甚至可以擁有域的“主”枚舉黎炉,以及每個域的場景的子枚舉枝秤。
最后,scene coordinator在場景之間處理轉(zhuǎn)換慷嗜。每個視圖模型知道協(xié)調(diào)器并能夠請求它來推送一個場景淀弹。
Coordinating scenes 383
當(dāng)開發(fā)一個圍繞MVVM的構(gòu)架時,最讓人迷惑的問題是:“如何做場景轉(zhuǎn)換庆械?”薇溃。這個問題有很多答案,因為每個架構(gòu)都有不同的做法缭乘。一些使用視圖控制器沐序,因為需要實例化其他的視圖控制器;一些使用router堕绩,它是一個用來連接視圖模型的特殊對象策幼。
Transitioning to another scene 383
本章的作者推薦一個簡單的解決方案,它是被證明是有效的奴紧,并已經(jīng)使用它開發(fā)了許多應(yīng)用程序:
- 一個視圖模型為下一個場景創(chuàng)建視圖模型特姐。
- 第一個視圖模型通過調(diào)用場景協(xié)調(diào)器來啟動向下一個場景的轉(zhuǎn)換。
- 場景協(xié)調(diào)器使用場景枚舉的擴(kuò)展函數(shù)實例化視圖控制器黍氮。
- 下一步唐含,它綁定控制器到下一個視圖模型。
- 最后滤钱,它呈現(xiàn)了下一個場景的視圖控制器觉壶。
通過這種結(jié)構(gòu),您可以將視圖模型與使用它們的視圖控制器完全隔離件缸,并將它們與從可以找到下一個視圖控制器的細(xì)節(jié)地方進(jìn)行隔離铜靶。 在本章的后面,您將看到如何使用Action模式來封裝上述步驟1和2他炊,并啟動轉(zhuǎn)換争剿。
Note:你總是調(diào)用場景協(xié)調(diào)器的transition(to:type:)和pop()函數(shù)來在場景間轉(zhuǎn)換是很總要的,因為協(xié)調(diào)器需要持續(xù)跟蹤哪一個視圖控制器在最前面痊末,尤其是以模態(tài)方式呈現(xiàn)場景時蚕苇。不要使用自動的segues。
The scene coordinator 384
場景協(xié)調(diào)器通過 SceneCoordinatorType協(xié)議來定義凿叠。一個具體的SceneCoordinator實現(xiàn)被提供來運行程序涩笤。你也能夠開發(fā)一個測試實現(xiàn)偽裝轉(zhuǎn)換嚼吞。
SceneCoordinatorType協(xié)議(已經(jīng)在起始項目中提供了),是簡單而高效的:
protocol SceneCoordinatorType {
init(window: UIWindow)
/// transition to another scene
@discardableResult
func transition(to scene: Scene, type: SceneTransitionType) -> Observable<Void>
/// pop scene from navigation stack or dismiss current modal
@discardableResult
func pop(animated: Bool) -> Observable<Void>
}
transition(to:type:) 和pop(animated:)這兩個函數(shù)讓你實現(xiàn)了所有你需要的轉(zhuǎn)換:push蹬碧,pop舱禽, modal和dismiss。
SceneCoordinator.swift中的具體實現(xiàn)顯示了使用RxSwift攔截委托消息的一些有趣的情況恩沽。 兩個轉(zhuǎn)換調(diào)用被設(shè)計為返回一個不發(fā)出任何東西的Observable <Void>誊稚,并在轉(zhuǎn)換完成后完成。 您可以訂閱它進(jìn)行進(jìn)一步的操作罗心,因為它的工作原理就像完成回調(diào)里伯。
為了實現(xiàn)這一點,項目中包含的代碼創(chuàng)建了一個UINavigationController DelegateProxy渤闷,一個RxSwift委托疾瓮,可以在將消息轉(zhuǎn)發(fā)給實際代理時攔截消息:
_ = navigationController.rx.delegate
.sentMessage(#selector(UINavigationControllerDelegate.navigationController(_:didShow:animated:)))
.map { _ in }
.bindTo(subject)
在 transition(to:type:)方法的底部找到的技巧,是將此訂閱綁定到返回給調(diào)用者的Subject:
return subject.asObservable()
.take(1)
.ignoreElements()
返回的observable將取得至多一個發(fā)送的元素來處理導(dǎo)航的情況飒箭,但不會轉(zhuǎn)發(fā)和完成爷贫。
Note:由于導(dǎo)航委托代理的無限訂閱,您可能會質(zhì)疑此構(gòu)造的內(nèi)存安全性补憾。 這是完全安全的:返回的observable取得最多一個元素,然后完成卷员。當(dāng)完成后盈匾,它會銷毀其訂閱。 如果沒有訂閱返回的observable毕骡,則該subject從內(nèi)存中銷毀削饵,其訂閱也將終止。
Passing data back 385
將數(shù)據(jù)從場景傳遞到前一個數(shù)據(jù)哺眯,例如當(dāng)場景以modally顯示時厢拭,使用RxSwift會很容易零远。 呈現(xiàn)的視圖模型實例化了呈現(xiàn)場景的視圖模型,因此可以訪問它并且可以建立通信劈伴。 為獲得最佳效果,您可以使用以下三種技術(shù)之一:
- 在第一(呈現(xiàn))視圖模型可以訂閱第二(呈現(xiàn))視圖模型中暴露的Observable握爷。當(dāng)?shù)诙€視圖模型解除顯示時跛璧,它可以在observable上發(fā)出一個或多個元素的結(jié)果。
- 將Observer對象(例如Variable或Subject)傳遞給所呈現(xiàn)的視圖模型新啼,該模型將使用此對象來發(fā)出一個或多個元素追城。
- 將一個或多個 Actions傳遞給所呈現(xiàn)的視圖模型,以適當(dāng)?shù)慕Y(jié)果執(zhí)行燥撞。
這些技術(shù)給予出色的可測試性座柱,并幫助您避免在模型之間使用弱引用玩游戲迷帜。 添加編輯任務(wù)視圖控制器時,您將看到本章后面的示例色洞。
Kicking off the first scene 386
關(guān)于使用協(xié)調(diào)場景模型的最終細(xì)節(jié)在啟動階段; 您需要通過引入第一個場景來啟動場景的顯示戏锹。 這是您在應(yīng)用程序委托中執(zhí)行的一個方法。
打開AppDelegate.swift并增加下面代碼到 application(_:didFinishLaunchingWithOptions:):
let service = TaskService()
let sceneCoordinator = SceneCoordinator(window: window!)
第一步是準(zhǔn)備與協(xié)調(diào)器一起所需的所有服務(wù)锋玲。 然后實例化第一個視圖模型景用,并指示協(xié)調(diào)器將其設(shè)置為root。
let tasksViewModel = TasksViewModel(taskService: service, coordinator:
sceneCoordinator)
let firstScene = Scene.tasks(tasksViewModel)
sceneCoordinator.transition(to: firstScene, type: .root)
那很簡單惭蹂! 這種技術(shù)是很酷的事情伞插,如果需要,您可以使用不同的啟動場景; 例如盾碗,第一次用戶打開您的應(yīng)用程序時運行的教程媚污。
現(xiàn)在您已經(jīng)完成了初始場景的設(shè)置,您可以查看各個視圖控制器廷雅。
Binding the tasks list with RxDataSources 386
在第18章“RxCocoa數(shù)據(jù)源”中耗美,您了解到在RxCocoa中內(nèi)置的UITableView和UICollectionView響應(yīng)式擴(kuò)展。 在本章中航缀,您將學(xué)習(xí)如何使用RxDataSources商架,這是RxSwiftCommunity提供的框架,最初由RxSwift的創(chuàng)始人Krunoslav Zaher開發(fā)芥玉。
這個框架不屬于RxCocoa的原因主要是它比RxCocoa提供的簡單擴(kuò)展更復(fù)雜和更深入蛇摸。
但是為什么要在RxCocoa的內(nèi)置綁定中使用RxDataSources?
RxDataSource提供了以下特性:
- 支持分段表和集合視圖灿巧。
- 優(yōu)化的重載赶袄,只需重新加載更改的內(nèi)容,例如刪除抠藕,插入和更新饿肺,這得益于有效的差異化算法。
- 可配置的動畫盾似,用于刪除敬辣,插入和更新。
- 支持section和item動畫颜说。
在此情況下购岗,采用RxDataSources將提供自動動畫,而無需任何工作门粪。我們的目標(biāo)是將檢查項目移動到任務(wù)列表末尾的“已檢查”部分喊积。
RxDataSources的不足之處在于它比基本的RxCocoa綁定更難理解。 您可以傳遞一個section model數(shù)組玄妈,而不是將一組items傳遞給表或集合視圖乾吻。 section model定義了部分標(biāo)題(如果有的話)以及每個項目的數(shù)據(jù)模型髓梅。
開始使用RxDataSources的最簡單方法是使用SectionModel或AnimatableSectionModel的通用類型作為您的section的類型。 因為你想要動畫的項目绎签,你可以使用 AnimatableSectionModel.枯饿。 您可以使用使用泛型類來簡單地指定section的類型信息和items數(shù)組。
打開TasksViewModel.swift并將其添加到頂部:
typealias TaskSection = AnimatableSectionModel<String, TaskItem>
這將您的section類型定義為具有String類型的section模型(您只需要一個標(biāo)題)诡必,并將section內(nèi)容定義為TaskItem元素的數(shù)組奢方。
使用RxDataSources的唯一限制是,section中使用的每個類型都必須符合IdentifiableType和Equatable協(xié)議爸舒。 IdentifiableType聲明一個唯一的標(biāo)識符(在同一具體類型的對象中是唯一的)蟋字,以便RxDataSources唯一標(biāo)識對象。 Equatable允許它比較對象來檢測相同唯一對象的兩個副本之間的變化扭勉。
Realm對象已經(jīng)符合Equatable協(xié)議(參見下面的注意事項)鹊奖。 現(xiàn)在,您只需要將TaskItem聲明為符合IdentifiableType涂炎。 打開TaskItem.swift并添加以下擴(kuò)展名:
extension TaskItem: IdentifiableType {
var identity: Int {
return self.isInvalidated ? 0 : uid
}
}
該代碼通過Realm數(shù)據(jù)庫檢查對象的有效性忠聚。 刪除任務(wù)時會發(fā)生這種情況; 任何以前從數(shù)據(jù)庫中查詢的活動副本都將無效。
Note:在您的情況下唱捣,更改檢測有點難度两蟀,因為Realm對象是類類型,而不是值類型震缭。 對數(shù)據(jù)庫的任何更新立即反映在對象屬性中垫竞,這使得RxDataSources的比較變得困難。 事實上蛀序,Realm的Equatable協(xié)議的實現(xiàn)很快,因為它只檢查兩個對象是否引用相同的存儲對象活烙。 有關(guān)此特定問題的解決方案徐裸,請參閱下面的“任務(wù)單元”部分。
現(xiàn)在啸盏,您需要將您的任務(wù)列表暴露為observable重贺。 您將使用TaskService的任務(wù)observable,感謝RxRealm回懦,在任務(wù)列表中發(fā)生更改時會自動發(fā)出气笙。 您的目標(biāo)是分離任務(wù)列表,如下所示:
- Due(未選中)任務(wù)怯晕,先按最后添加排序
- Done(已檢查)任務(wù)潜圃,按檢查數(shù)據(jù)排序(最后檢查)
將其添加到TasksViewModel類中:
var sectionedItems: Observable<[TaskSection]> {
return self.taskService.tasks()
.map { results in
let dueTasks = results
.filter("checked == nil")
.sorted(byKeyPath: "added", ascending: false)
let doneTasks = results
.filter("checked != nil")
.sorted(byKeyPath: "checked", ascending: false)
return [
TaskSection(model: "Due Tasks", items: dueTasks.toArray()),
TaskSection(model: "Done Tasks", items: doneTasks.toArray())
]
}
}
通過返回一個包含兩個TaskSection元素的數(shù)組,您用兩個sections自動創(chuàng)建一個列表舟茶。
現(xiàn)在到TasksViewController谭期。 這里會發(fā)生一些有趣的操作堵第,將sectionedable observable綁定到表格視圖。 第一步是創(chuàng)建適合與RxDataSources一起使用的數(shù)據(jù)源隧出。 對于表格視圖踏志,它可以是以下之一:
- RxTableViewSectionedReloadDataSource<SectionType>
- RxTableViewSectionedAnimatedDataSource<SectionType>
Reload類型不是很先進(jìn)。 當(dāng)section observable訂閱發(fā)出一個新的sections列表,胀瞪,它只是重新加載表针余。
動畫類型是您想要的。 它不僅執(zhí)行局部重載凄诞,還可以動畫化每個變化圆雁。 將以下dataSource屬性添加到TasksViewController類中:
let dataSource = RxTableViewSectionedAnimatedDataSource<TaskSection>()
與RxCocoa支持的內(nèi)置表格視圖的主要區(qū)別是您設(shè)置數(shù)據(jù)源對象來顯示每個單元格類型,而不是在訂閱中執(zhí)行幔摸。
在TasksViewController中摸柄,添加一個函數(shù)到數(shù)據(jù)源的“skin”:
fileprivate func configureDataSource() {
dataSource.titleForHeaderInSection = { dataSource, index in
dataSource.sectionModels[index].model
}
dataSource.configureCell = {
[weak self] dataSource, tableView, indexPath, item in
let cell = tableView.dequeueReusableCell(withIdentifier:
"TaskItemCell", for: indexPath) as! TaskItemTableViewCell
if let strongSelf = self {
cell.configure(with: item, action:
strongSelf.viewModel.onToggle(task: item))
}
return cell
}
}
正如您在第18章“RxCocoa Data Sources”中學(xué)到的,當(dāng)將observable綁定到表格或集合視圖時既忆,您可以根據(jù)需要提供閉包來生成和配置每個單元格驱负。 RxDataSources的工作方式相同,但配置全部在“數(shù)據(jù)源”對象中執(zhí)行患雇。
有關(guān)此配置代碼的一個詳細(xì)信息是該MVVM架構(gòu)的關(guān)鍵跃脊。 注意您如何將Action傳遞給配置函數(shù)?
傳回視圖模型苛吱,這是您設(shè)計處理來至單元格觸發(fā)動作的方式酪术。
它非常像閉包,除了由視圖模型提供的動作翠储,視圖控制器限制將單元格與動作連接起來的作用绘雁。
最后,它的工作原理如下:
有趣的部分是援所,除了將動作分配給其按鈕(見下文)之外庐舟,單元本身不必了解視圖模型本身的任何內(nèi)容。
NOTE:titleForHeaderInSection閉包返回字符串作為section headers的標(biāo)題住拭。 這是創(chuàng)建section headers的最簡單的例子挪略。 如果您想要更詳細(xì)定制的內(nèi)容,可以通過設(shè)置dataSource.supplementaryViewFactory來為UICollectionElementKindSectionHeader類返回一個適當(dāng)?shù)腢ICollectionReusableView來進(jìn)行配置滔岳。
由于在viewDidLoad()里設(shè)置表格視圖為自動高度模式杠娱,因此這是完成表格配置的好地方。 RxDataSources的唯一需求是數(shù)據(jù)源配置必須在綁定observable之前完成谱煤。
在 viewDidLoad()中增加:
configureDataSource()
最后摊求,在bindViewModel()函數(shù)中,通過它的數(shù)據(jù)源刘离,將視圖模型的sectionedItems observable綁定到表格視圖中:
viewModel.sectionedItems
.bindTo(tableView.rx.items(dataSource: dataSource))
.disposed(by: self.rx_disposeBag)
你完成了第一個控制器睹簇! 您可以對dataSource對象中的每個更改類型使用不同的動畫奏赘。 現(xiàn)在將它們保留為默認(rèn)值。
用于在“任務(wù)”列表中顯示項目的單元格是一個需要關(guān)注的情況太惠。 除了使用Action模式將“checkmark toggled”信息轉(zhuǎn)發(fā)到視圖模型(見上圖)之外磨淌,還必須處理在顯示期間可能會發(fā)生更改底層對象(一個Realm對象實例)。
幸運的是凿渊,RxSwift可以解決這個問題梁只。 由于存儲在Realm數(shù)據(jù)庫中的對象使用動態(tài)屬性,因此可以使用KVO進(jìn)行觀察埃脏。 使用RxSwift搪锣,您可以使用 object.rx.observe(class, propertyName)從屬性更改創(chuàng)建可觀察序列!
Binding the Task cell 391
您將把這個技術(shù)應(yīng)用到TaskTableViewCell彩掐。 打開類文件并添加以下內(nèi)容到 configure(with:action:)方法:
button.rx.action = action
您首先將“toggle checkmark”操作綁定到復(fù)選標(biāo)記按鈕构舟。 有關(guān)Action模式的更多詳細(xì)信息,請參閱第19章“Action”堵幽。
現(xiàn)在綁定標(biāo)題字符串和“已檢查”狀態(tài)圖像:
item.rx.observe(String.self, "title")
.subscribe(onNext: { [weak self] title in
self?.title.text = title
})
.disposed(by: disposeBag)
item.rx.observe(Date.self, "checked")
.subscribe(onNext: { [weak self] date in
let image = UIImage(named: (date == nil) ? "ItemNotChecked" :
"ItemChecked")
self?.button.setImage(image, for: .normal)
})
.disposed(by: disposeBag)
在這里狗超,您可以相應(yīng)地單獨觀察這兩個屬性并更新單元格內(nèi)容。由于您在訂閱時立即收到初始值朴下,您可以確信單元格始終是最新的努咐。
最后,當(dāng)單元格被表格視圖重用時殴胧,別忘了處理您的訂閱渗稍, 不然它會讓你大吃一驚! 添加以下內(nèi)容:
override func prepareForReuse() {
button.rx.action = nil
disposeBag = DisposeBag()
super.prepareForReuse()
}
這是清理和準(zhǔn)備單元格重用的正確方法团滥。 一直非常小心不要留著懸空的訂閱竿屹! 在單元格的這個情況下,由于單元格本身被重用灸姊,所以您必須小心這一點羔沙。
構(gòu)建并運行應(yīng)用程序。 您應(yīng)該可以看到默認(rèn)的任務(wù)列表厨钻。 勾選一個,您將看到由RxDataSources的差異引擎自動生成的漂亮動畫坚嗜!
Editing tasks 392
解決的另一個問題是創(chuàng)建和修改任務(wù)夯膀。 您要在創(chuàng)建或編輯任務(wù)時呈現(xiàn)模態(tài)視圖控制器,并且操作(如更新或刪除)應(yīng)傳回任務(wù)列表視圖模型苍蔬。 雖然在這種情況下不是絕對必要的诱建,因為本地可以處理更改,任務(wù)列表將自動更新碟绑,感謝Realm俺猿,重要的是您學(xué)習(xí)了將信息傳遞回一系列場景的模式茎匠。
實現(xiàn)此目的的主要方法是使用可信的Action模式。 這是它的計劃:
- 準(zhǔn)備編輯場景時押袍,在初始化傳遞一個或多個動作诵冒。
- 編輯場景執(zhí)行其工作,并在退出時執(zhí)行相應(yīng)的操作(更新或取消)谊惭。
- 呼叫者可以通過不同的動作取決于它的上下文汽馋,編輯場景將不會知道差異。 在創(chuàng)建時通過“刪除”動作以取消刪除操作(或無操作)圈盔。
當(dāng)您將其應(yīng)用于您自己的應(yīng)用程序時豹芯,您會發(fā)現(xiàn)這種模式非常靈活。 在呈現(xiàn)模態(tài)場景時驱敲,特別有用铁蹈,也可以傳達(dá)要通過合成結(jié)果集的多個場景的結(jié)果。
是時候把它付諸實踐了众眨。 將以下函數(shù)添加到TasksViewModel中:
func onCreateTask() -> CocoaAction {
return CocoaAction { _ in
return self.taskService
.createTask(title: "")
.flatMap { task -> Observable<Void> in
let editViewModel = EditTaskViewModel(task: task,
coordinator: self.sceneCoordinator,
updateAction: self.onUpdateTitle(task: task),
cancelAction: self.onDelete(task: task))
return self.sceneCoordinator.transition(to:
Scene.editTask(editViewModel), type: .modal)
}
}
}
Note:由于self是一個結(jié)構(gòu)體握牧,所以action得到了自己的“copy”結(jié)構(gòu)體(由Swift優(yōu)化為一個引用),沒有循環(huán)引用 ——沒有內(nèi)存泄漏的風(fēng)險围辙! 這就是為什么你在這里看不到[weak self]或[unowned self]我碟,它不適用于值類型。
這是您將綁定到任務(wù)列表場景右上角的“+”按鈕的操作姚建。 下面是它的作用:
- 創(chuàng)建一個新的新任務(wù)項目矫俺。
- 如果創(chuàng)建成功,實例化一個新的EditTaskViewModel掸冤,并與updateAction一起傳遞厘托,updateAction更新新任務(wù)項目的標(biāo)題以及一個刪除任務(wù)項目的cancelAction。 由于剛剛創(chuàng)建稿湿,所以取消應(yīng)在邏輯上刪除任務(wù)铅匹。
Note:由于Action返回可觀察的序列,因此您可以將整個創(chuàng)建編輯過程整合到單個序列中饺藤,一旦編輯任務(wù)場景關(guān)閉包斑,該過程就會完成。 由于一個Action保持鎖定狀態(tài)涕俗,直到執(zhí)行observable完成罗丰,所以不可能同時在不經(jīng)意間增加編輯器兩被的時間。 酷再姑!
現(xiàn)在將操作綁定到TasksViewController的bindViewModel()函數(shù)上的“+”按鈕:
newTaskButton.rx.action = viewModel.onCreateTask()
接下來萌抵,轉(zhuǎn)到EditTaskViewModel.swift并填充初始化程序。 將此代碼添加到 init(task:coordinator:updateAction:cancelAction:):
onUpdate.executionObservables
.take(1)
.subscribe(onNext: { _ in
coordinator.pop()
})
.disposed(by: disposeBag)
Note:為了允許大部分代碼進(jìn)行編譯,onUpdate和onCancel屬性被定義為強(qiáng)制解包的可選值绍填。 您現(xiàn)在可以刪除感嘆號霎桅。
上面做了什么? 除了將onUpdate操作設(shè)置為傳遞給初始化程序的操作之外讨永,它還會在動作執(zhí)行時訂閱動作的執(zhí)行Observables序列滔驶,該序列發(fā)出新的可觀察值。 由于該操作將被綁定到OK按鈕住闯,您只能看到它執(zhí)行一次瓜浸。 當(dāng)這種情況發(fā)生時,您pop()當(dāng)前場景比原,并且場景協(xié)調(diào)器將關(guān)閉它插佛。
對于“取消”按鈕,您需要進(jìn)行不同的操作量窘。 刪除現(xiàn)有的onCancel = cancelAction分配; 你會做一些更聰明的事情雇寇。
由于初始化程序接收到的操作是可選的,因為調(diào)用者在取消時可能沒有任何操作蚌铜,您需要生成一個新的Action锨侯。 因此,這將是pop()場景的時機(jī):
onCancel = CocoaAction {
if let cancelAction = cancelAction {
cancelAction.execute()
}
return coordinator.pop()
}
最后冬殃,移動到EditTaskViewController(在EditTaskViewController.swift)類中以完成UI綁定囚痴。 將其添加到bindViewModel()中:
cancelButton.rx.action = viewModel.onCancel
okButton.rx.tap
.withLatestFrom(titleView.rx.text.orEmpty)
.subscribe(viewModel.onUpdate.inputs)
.disposed(by: rx_disposeBag)
當(dāng)用戶點擊OK按鈕時,您需要處理關(guān)于UI的所有操作是將文本視圖內(nèi)容傳遞給onUpdate操作审葬。 您正在利用Action的輸入觀察者深滚,它可以直接管理值以執(zhí)行該操作。
構(gòu)建并運行應(yīng)用程序涣觉。 創(chuàng)建新項目并更新其標(biāo)題以查看所有操作痴荐。
最后一件事就是增加現(xiàn)有的項目。 為此官册,您需要一個不是臨時的新動作生兆;請記住,除了通過訂閱之外膝宁,actions必須被引用鸦难,否則將被釋放。 如第19章所述员淫,這是一個經(jīng)澈媳危混淆的來源。
在TasksViewModel中創(chuàng)建一個新的惰性變量:
lazy var editAction: Action<TaskItem, Void> = { this in
return Action { task in
let editViewModel = EditTaskViewModel(
task: task,
coordinator: this.sceneCoordinator,
updateAction: this.onUpdateTitle(task: task)
)
return this.sceneCoordinator.transition(to:
Scene.editTask(editViewModel), type: .modal)
}
}(self)
注意:由于self是一個結(jié)構(gòu)體满粗,因此不能創(chuàng)建weak或unowned引用。 替代地愚争,將self傳遞給初始化懶惰變量的閉包或函數(shù)映皆。
現(xiàn)在挤聘,在TaskViewController.swift中,您可以在TaskViewController的bindViewModel()中綁定此操作捅彻。 加:
tableView.rx.itemSelected
.map { [unowned self] indexPath in
try! self.dataSource.model(at: indexPath) as! TaskItem
}
.subscribe(viewModel.editAction.inputs)
.disposed(by: rx_disposeBag)
您正在使用dataSource對獲取的模型對象與接收到的IndexPath匹配组去,然后將其導(dǎo)入操作的輸入。 簡單步淹!
構(gòu)建并運行應(yīng)用程序:您現(xiàn)在可以創(chuàng)建和編輯任務(wù)从隆!萬歲!