該文章屬於<簡書 — 劉小壯>原創,轉載請註明:
<簡書 — 劉小壯> http://www.jianshu.com/p/01f36026da7d
在之前的文章中,已經講了很多關於CoreData
使用相關的知識點。這篇文章中主要講兩個方面,NSFetchedResultsController
和版本遷移。文章題目中雖然有“高級”兩個字,其實講的東西並不高級,只是因爲上一篇文章中東西太多了,把兩個較複雜的知識點挪到這篇文章中。😓
文章中如有疏漏或錯誤,還請各位及時提出,謝謝!😊
NSFetchedResultsController
在開發過程中會經常用到UITableView
這樣的視圖類,這些視圖類需要自己管理其數據源,包括網絡獲取、本地存儲都需要寫代碼進行管理。
而在CoreData
中提供了NSFetchedResultsController
類(fetched results controller
,也叫FRC
),FRC
可以管理UITableView
或UICollectionView
的數據源。這個數據源主要指本地持久化的數據,也可以用這個數據源配合着網絡請求數據一起使用,主要看業務需求了。
本篇文章會使用UITableView
作爲視圖類,配合NSFetchedResultsController
進行後面的演示,UICollectionView
配合NSFetchedResultsController
的使用也是類似,這裏就不都講了。
簡單介紹
就像上面說到的,NSFetchedResultsController
就像是上面兩種視圖的數據管理者一樣。FRC
可以監聽一個MOC
的改變,如果MOC
執行了託管對象的增刪改操作,就會對本地持久化數據發生改變,FRC
就會回調對應的代理方法,回調方法的參數會包括執行操作的類型、操作的值、indexPath
等參數。
實際使用時,通過FRC
“綁定”一個MOC
,將UITableView
嵌入在FRC
的執行流程中。在任何地方對這個“綁定”的MOC
存儲區做修改,都會觸發FRC
的回調方法,在FRC
的回調方法中嵌入UITableView
代碼並做對應修改即可。
由此可以看出FRC
最大優勢就是,始終和本地持久化的數據保持統一。只要本地持久化的數據發生改變,就會觸發FRC
的回調方法,從而在回調方法中更新上層數據源和UI
。這種方式講的簡單一點,就可以叫做數據帶動UI。
但是需要注意一點,在FRC
的初始化中傳入了一個MOC
參數,FRC
只能監測傳入的MOC
發生的改變。假設其他MOC
對同一個存儲區發生了改變,FRC
則不能監測到這個變化,不會做出任何反應。
所以使用FRC
時,需要注意FRC
只能對一個MOC
的變化做出反應,所以在CoreData
持久化層設計時,儘量一個存儲區只對應一個MOC
,或設置一個負責UI
的MOC
,這在後面多線程部分會詳細講解。
修改模型文件結構
在寫代碼之前,先對之前的模型文件結構做一些修改。
講FRC
的時候,只需要用到Employee
這一張表,其他表和設置直接忽略。需要在Employee
原有字段的基礎上,增加一個String
類型的sectionName
字段,這個字段就是用來存儲section title
的,在下面的文章中將會詳細講到。
初始化FRC
下面例子是比較常用的FRC
初始化方式,初始化時指定的MOC
,還用之前講過的MOC
初始化代碼,UITableView
初始化代碼這裏也省略了,主要突出FRC
的初始化。
// 創建請求對象,並指明操作Employee表
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 設置排序規則,指明根據height字段升序排序
NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES];
request.sortDescriptors = @[heightSort];
// 創建NSFetchedResultsController控制器實例,並綁定MOC
NSError *error = nil;
fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:@"sectionName"
cacheName:nil];
// 設置代理,並遵守協議
fetchedResultController.delegate = self;
// 執行獲取請求,執行後FRC會從持久化存儲區加載數據,其他地方可以通過FRC獲取數據
[fetchedResultController performFetch:&error];
// 錯誤處理
if (error) {
NSLog(@"NSFetchedResultsController init error : %@", error);
}
// 刷新UI
[tableView reloadData];
在上面初始化FRC
時,傳入的sectionNameKeyPath:
參數,是指明當前託管對象的哪個屬性當做section
的title
,在本文中就是Employee
表的sectionName
字段爲section
的title
。從NSFetchedResultsSectionInfo
協議的indexTitle
屬性獲取這個值。
在sectionNameKeyPath:
設置屬性名後,就以這個屬性名作爲分組title
,相同的title
會被分到一個section
中。
初始化FRC
時參數managedObjectContext:
傳入了一個MOC
參數,FRC
只能監測這個傳入的MOC
發生的本地持久化改變。就像上面介紹時說的,其他MOC
對同一個持久化存儲區發生的改變,FRC
則不能監測到這個變化。
再往後面看到cacheName:
參數,這個參數我設置的是nil
。參數的作用是開啓FRC
的緩存,對獲取的數據進行緩存並指定一個名字。可以通過調用deleteCacheWithName:
方法手動刪除緩存。
但是這個緩存並沒有必要,緩存是根據NSFetchRequest
對象來匹配的,如果當前獲取的數據和之前緩存的相匹配則直接拿來用,但是在獲取數據時每次獲取的數據都可能不同,緩存不能被命中則很難派上用場,而且緩存還佔用着內存資源。
在FRC
初始化完成後,調用performFetch:
方法來同步獲取持久化存儲區數據,調用此方法後FRC
保存數據的屬性纔會有值。獲取到數據後,調用tableView
的reloadData
方法,會回調tableView
的代理方法,可以在tableView
的代理方法中獲取到FRC
的數據。調用performFetch:
方法第一次獲取到數據並不會回調FRC
代理方法。
代理方法
FRC
中包含UITableView
執行過程中需要的相關數據,可以通過FRC
的sections
屬性,獲取一個遵守<NSFetchedResultsSectionInfo>
協議的對象數組,數組中的對象就代表一個section
。
在這個協議中有如下定義,可以看出這些屬性和UITableView
的執行流程是緊密相關的。
@protocol NSFetchedResultsSectionInfo
/* Name of the section */
@property (nonatomic, readonly) NSString *name;
/* Title of the section (used when displaying the index) */
@property (nullable, nonatomic, readonly) NSString *indexTitle;
/* Number of objects in section */
@property (nonatomic, readonly) NSUInteger numberOfObjects;
/* Returns the array of objects in the section. */
@property (nullable, nonatomic, readonly) NSArray *objects;
@end // NSFetchedResultsSectionInfo
在使用過程中應該將FRC
和UITableView
相互嵌套,在FRC
的回調方法中嵌套UITableView
的視圖改變邏輯,在UITableView
的回調中嵌套數據更新的邏輯。這樣可以始終保證數據和UI的同步,在下面的示例代碼中將會演示FRC
和UITableView
的相互嵌套。
Table View Delegate
// 通過FRC的sections數組屬性,獲取所有section的count值
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return fetchedResultController.sections.count;
}
// 通過當前section的下標從sections數組中取出對應的section對象,並從section對象中獲取所有對象count
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return fetchedResultController.sections[section].numberOfObjects;
}
// FRC根據indexPath獲取託管對象,並給cell賦值
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identifier" forIndexPath:indexPath];
cell.textLabel.text = emp.name;
return cell;
}
// 創建FRC對象時,通過sectionNameKeyPath:傳遞進去的section title的屬性名,在這裏獲取對應的屬性值
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return fetchedResultController.sections[section].indexTitle;
}
// 是否可以編輯
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
// 這裏是簡單模擬UI刪除cell後,本地持久化區數據和UI同步的操作。在調用下面MOC保存上下文方法後,FRC會回調代理方法並更新UI
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// 刪除託管對象
Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
[context deleteObject:emp];
// 保存上下文環境,並做錯誤處理
NSError *error = nil;
if (![context save:&error]) {
NSLog(@"tableView delete cell error : %@", error);
}
}
}
上面是UITableView
的代理方法,代理方法中嵌套了FRC
的數據獲取代碼,這樣在刷新視圖時就可以保證使用最新的數據。並且在代碼中簡單實現了刪除cell
後,通過MOC
調用刪除操作,使本地持久化數據和UI
保持一致。
就像上面cellForRowAtIndexPath:
方法中使用的一樣,FRC
提供了兩個方法輕鬆轉換indexPath
和NSManagedObject
的對象,在實際開發中這兩個方法非常實用,這也是FRC
和UITableView
、UICollectionView
深度融合的表現。
- (id)objectAtIndexPath:(NSIndexPath *)indexPath;
- (nullable NSIndexPath *)indexPathForObject:(id)object;
Fetched Results Controller Delegate
// Cell數據源發生改變會回調此方法,例如添加新的託管對象等
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(nullable NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(nullable NSIndexPath *)newIndexPath {
switch (type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[tableView insertRowsAtIndexPaths:@[newIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeUpdate: {
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
cell.textLabel.text = emp.name;
}
break;
}
}
// Section數據源發生改變回調此方法,例如修改section title等。
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id <NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch (type) {
case NSFetchedResultsChangeInsert:
[tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationAutomatic];
break;
default:
break;
}
}
// 本地數據源發生改變,將要開始回調FRC代理方法。
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[tableView beginUpdates];
}
// 本地數據源發生改變,FRC代理方法回調完成。
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[tableView endUpdates];
}
// 返回section的title,可以在這裏對title做進一步處理。這裏修改title後,對應section的indexTitle屬性會被更新。
- (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName {
return [NSString stringWithFormat:@"sectionName %@", sectionName];
}
上面就是當本地持久化數據發生改變後,被回調的FRC
代理方法的實現,可以在對應的實現中完成自己的代碼邏輯。
在上面的章節中講到刪除cell
後,本地持久化數據同步的問題。在刪除cell
後在tableView
代理方法的回調中,調用了MOC
的刪除方法,使本地持久化存儲和UI
保持同步,並回調到下面的FRC
代理方法中,在代理方法中對UI
做刪除操作,這樣一套由UI的改變引發的刪除流程就完成了。
目前爲止已經實現了數據和UI
的雙向同步,即UI
發生改變後本地存儲發生改變,本地存儲發生改變後UI
也隨之改變。可以通過下面添加數據的代碼來測試一下,NSFetchedResultsController
就講到這裏了。
- (void)addMoreData {
Employee *employee = [NSEntityDescription insertNewObjectForEntityForName:@"Employee" inManagedObjectContext:context];
employee.name = [NSString stringWithFormat:@"lxz 15"];
employee.height = @(15);
employee.brithday = [NSDate date];
employee.sectionName = [NSString stringWithFormat:@"3"];
NSError *error = nil;
if (![context save:&error]) {
NSLog(@"MOC save error : %@", error);
}
}
版本遷移
CoreData
版本遷移的方式有很多,一般都是先在Xcode
中,原有模型文件的基礎上,創建一個新版本的模型文件,然後在此基礎上做不同方式的版本遷移。
本章節將會講三種不同的版本遷移方案,但都不會講太深,都是從使用的角度講起,可以滿足大多數版本遷移的需求。
爲什麼要版本遷移?
在已經運行程序並通過模型文件生成數據庫後,再對模型文件進行的修改,如果只是修改已有實體屬性的默認值、最大最小值、Fetch Request
等屬性自身包含的參數時,並不會發生錯誤。如果修改模型文件的結構,或修改屬性名、實體名等,造成模型文件的結構發生改變,這樣再次運行程序就會導致崩潰。
在開發測試過程中,可以直接將原有程序卸載就可以解決這個問題,但是本地之前存儲的數據也會消失。如果是線上程序,就涉及到版本遷移的問題,否則會導致崩潰,並提示如下錯誤:
CoreData: error: Illegal attempt to save to a file that was never opened. "This NSPersistentStoreCoordinator has no persistent stores (unknown). It cannot perform a save operation.". No last error recorded.
然而在需求不斷變化的過程中,後續版本肯定會對原有的模型文件進行修改,這時就需要用到版本遷移的技術,下面開始講版本遷移的方案。
創建新版本模型文件
本文中講的幾種版本遷移方案,在遷移之前都需要對原有的模型文件創建新版本。
選中需要做遷移的模型文件 -> 點擊菜單欄Editor -> Add Model Version -> 選擇基於哪個版本的模型文件(一般都是選擇目前最新的版本),新建模型文件完成。
對於新版本模型文件的命名,我在創建新版本模型文件時,一般會拿當前工程版本號當做後綴,這樣在模型文件版本比較多的時候,就可以很容易將模型文件版本和工程版本對應起來。
添加完成後,會發現之前的模型文件會變成一個文件夾,裏面包含着多個模型文件。
在新建的模型文件中,裏面的文件結構和之前的文件結構相同。後續的修改都應該在新的模型文件上,之前的模型文件不要再動了,在修改完模型文件後,記得更新對應的模型類文件。
基於新的模型文件,對Employee
實體做如下修改,下面的版本遷移也以此爲例。
添加一個String
類型的屬性,設置屬性名爲sectionName
。
此時還應該選中模型文件,設置當前模型文件的版本。這裏選擇將最新版本設置爲剛纔新建的1.1.0版本
,模型文件設置工作完成。
Show The File Inspector -> Model Version -> Current 設置爲最新版本。
對模型文件的設置已經完成了,接下來系統還要知道我們想要怎樣遷移數據。在遷移過程中可能會存在多種可能,蘋果將這個靈活性留給了我們完成。剩下要做的就是編寫遷移方案以及細節的代碼。
輕量級版本遷移
輕量級版本遷移方案非常簡單,大多數遷移工作都是由系統完成的,只需要告訴系統遷移方式即可。在持久化存儲協調器(PSC
)初始化對應的持久化存儲(NSPersistentStore
)對象時,設置options
參數即可,參數是一個字典。PSC
會根據傳入的字典,自動推斷版本遷移的過程。
字典中設置的key:
-
NSMigratePersistentStoresAutomaticallyOption
設置爲YES
,CoreData
會試着把低版本的持久化存儲區遷移到最新版本的模型文件。 -
NSInferMappingModelAutomaticallyOption
設置爲YES
,CoreData
會試着以最爲合理地方式自動推斷出源模型文件的實體中,某個屬性到底對應於目標模型文件實體中的哪一個屬性。
版本遷移的設置是在創建MOC
時給PSC
設置的,爲了使代碼更直觀,下面只給出發生變化部分的代碼,其他MOC
的初始化代碼都不變。
// 設置版本遷移方案
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES};
// 創建持久化存儲協調器,並將遷移方案的字典當做參數傳入
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
修改實體名
假設需要對已存在實體進行改名操作,需要將重命名後的實體Renaming ID
,設置爲之前的實體名。下面是Employee
實體進行操作。
修改後再使用實體時,應該將實體名設爲最新的實體名,這裏也就是Employee2
,而且數據庫中的數據也會遷移到Employee2
表中。
Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context];
emp.name = @"lxz";
emp.brithday = [NSDate date];
emp.height = @1.9;
[context save:nil];
Mapping Model 遷移方案
輕量級遷移方案只是針對增加和改變實體、屬性這樣的一些簡單操作,假設有更復雜的遷移需求,就應該使用Xcode
提供的遷移模板(Mapping Model
)。通過Xcode
創建一個後綴爲.xcmappingmodel
的文件,這個文件是專門用來進行數據遷移用的,一些變化關係也會體現在模板中,看起來非常直觀。
這裏還以上面更改實體名,並遷移實體數據爲例子,將Employee
實體遷移到Employee2
中。首先將Employee
實體改名爲Employee2
,然後創建Mapping Model
文件。
Command + N 新建文件 -> 選擇 Mapping Model -> 選擇源文件 Source Model -> 選擇目標文件 Target Model -> 命名 Mapping Model 文件名 -> Create 創建完成。
現在就創建好一個Mapping Model
文件,文件中顯示了實體、屬性、Relationships
,源文件和目標文件之間的關係。實體命名是EntityToEntity
的方式命名的,實體包含的屬性和關聯關係,都會被添加到遷移方案中(Entity Mapping
,Attribute Mapping
,Relationship Mapping
)。
在遷移文件的下方是源文件和目標文件的關係。
在上面圖中改名後的Employee2
實體並沒有遷移關係,由於是改名後的實體,系統還不知道實體應該怎樣做遷移。所以選中Mapping Model
文件的Employee2 Mappings
,可以看到右側邊欄的Source
爲invalid value
。因爲要從Employee
實體遷移數據過來,所以將其選擇爲Employee
,遷移關係就設置完成了。
設置完成後,還應該將之前EmployeeToEmployee
的Mappings
刪除,因爲這個實體已經被Employee2
替代,它的Mappings
也被Employee2 Mappings
所替代,否則會報錯。
在實體的遷移過程中,還可以通過設置Predicate
的方式,來簡單的控制遷移過程。例如只需要遷移一部分指定的數據,就可以通過Predicate
來指定。可以直接在右側Filter Predicate
的位置設置過濾條件,格式是$source.height < 100
,$source
代表數據源的實體。
更復雜的遷移需求
如果還存在更復雜的遷移需求,而且上面的遷移方式不能滿足,可以考慮更復雜的遷移方式。假設要在遷移過程中,對遷移的數據進行更改,這時候上面的遷移方案就不能滿足需求了。
對於上面提到的問題,在Mapping Model
文件中選中實體,可以看到Custom Policy
這個選項,選項對應的是NSEntityMigrationPolicy
的子類,可以創建並設置一個子類,並重寫這個類的方法來控制遷移過程。
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;
版本遷移總結
版本遷移在需求的變更中肯定是要發生的,但是我們應該儘量避免這樣的情況發生。在最開始設計模型文件數據結構的時候,就應該設計一個比較完善並且容易應對變化的結構,這樣後面就算髮生變化也不會對結構主體造成大的改動。
好多同學都問我有Demo
沒有,其實文章中貼出的代碼組合起來就是個Demo
。後來想了想,還是給本系列文章配了一個簡單的Demo
,方便大家運行調試,後續會給所有博客的文章都加上Demo
。
Demo
只是來輔助讀者更好的理解文章中的內容,應該博客結合Demo
一起學習,只看Demo
還是不能理解更深層的原理。Demo
中幾乎每一行代碼都會有註釋,各位可以打斷點跟着Demo
執行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github
這兩天更新了一下文章,將CoreData
系列的六篇文章整合在一起,做了一個PDF
版的《CoreData Book》,放在我Github上了。PDF
上有文章目錄,方便閱讀。
如果你覺得不錯,請把PDF幫忙轉到其他羣裏,或者你的朋友,讓更多的人瞭解CoreData,衷心感謝!😁