該文章屬于劉小壯原創(chuàng),轉(zhuǎn)載請注明:劉小壯

在之前的文章中,已經(jīng)講了很多關(guān)于
CoreData使用相關(guān)的知識點。這篇文章中主要講兩個方面,NSFetchedResultsController和版本遷移。文章題目中雖然有“高級”兩個字,其實講的東西并不高級,只是因為上一篇文章中東西太多了,把兩個較復(fù)雜的知識點挪到這篇文章中。??
文章中如有疏漏或錯誤,還請各位及時提出,謝謝!??
NSFetchedResultsController
在開發(fā)過程中會經(jīng)常用到UITableView這樣的視圖類,這些視圖類需要自己管理其數(shù)據(jù)源,包括網(wǎng)絡(luò)獲取、本地存儲都需要寫代碼進行管理。
而在CoreData中提供了NSFetchedResultsController類(fetched results controller,也叫FRC),FRC可以管理UITableView或UICollectionView的數(shù)據(jù)源。這個數(shù)據(jù)源主要指本地持久化的數(shù)據(jù),也可以用這個數(shù)據(jù)源配合著網(wǎng)絡(luò)請求數(shù)據(jù)一起使用,主要看業(yè)務(wù)需求了。
本篇文章會使用UITableView作為視圖類,配合NSFetchedResultsController進行后面的演示,UICollectionView配合NSFetchedResultsController的使用也是類似,這里就不都講了。
簡單介紹
就像上面說到的,NSFetchedResultsController就像是上面兩種視圖的數(shù)據(jù)管理者一樣。FRC可以監(jiān)聽一個MOC的改變,如果MOC執(zhí)行了托管對象的增刪改操作,就會對本地持久化數(shù)據(jù)發(fā)生改變,FRC就會回調(diào)對應(yīng)的代理方法,回調(diào)方法的參數(shù)會包括執(zhí)行操作的類型、操作的值、indexPath等參數(shù)。
實際使用時,通過FRC“綁定”一個MOC,將UITableView嵌入在FRC的執(zhí)行流程中。在任何地方對這個“綁定”的MOC存儲區(qū)做修改,都會觸發(fā)FRC的回調(diào)方法,在FRC的回調(diào)方法中嵌入UITableView代碼并做對應(yīng)修改即可。
由此可以看出FRC最大優(yōu)勢就是,始終和本地持久化的數(shù)據(jù)保持統(tǒng)一。只要本地持久化的數(shù)據(jù)發(fā)生改變,就會觸發(fā)FRC的回調(diào)方法,從而在回調(diào)方法中更新上層數(shù)據(jù)源和UI。這種方式講的簡單一點,就可以叫做數(shù)據(jù)帶動UI。

但是需要注意一點,在FRC的初始化中傳入了一個MOC參數(shù),FRC只能監(jiān)測傳入的MOC發(fā)生的改變。假設(shè)其他MOC對同一個存儲區(qū)發(fā)生了改變,FRC則不能監(jiān)測到這個變化,不會做出任何反應(yīng)。
所以使用FRC時,需要注意FRC只能對一個MOC的變化做出反應(yīng),所以在CoreData持久化層設(shè)計時,盡量一個存儲區(qū)只對應(yīng)一個MOC,或設(shè)置一個負責(zé)UI的MOC,這在后面多線程部分會詳細講解。
修改模型文件結(jié)構(gòu)
在寫代碼之前,先對之前的模型文件結(jié)構(gòu)做一些修改。

講FRC的時候,只需要用到Employee這一張表,其他表和設(shè)置直接忽略。需要在Employee原有字段的基礎(chǔ)上,增加一個String類型的sectionName字段,這個字段就是用來存儲section title的,在下面的文章中將會詳細講到。
初始化FRC
下面例子是比較常用的FRC初始化方式,初始化時指定的MOC,還用之前講過的MOC初始化代碼,UITableView初始化代碼這里也省略了,主要突出FRC的初始化。
// 創(chuàng)建請求對象,并指明操作Employee表
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
// 設(shè)置排序規(guī)則,指明根據(jù)height字段升序排序
NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES];
request.sortDescriptors = @[heightSort];
// 創(chuàng)建NSFetchedResultsController控制器實例,并綁定MOC
NSError *error = nil;
fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:@"sectionName"
cacheName:nil];
// 設(shè)置代理,并遵守協(xié)議
fetchedResultController.delegate = self;
// 執(zhí)行獲取請求,執(zhí)行后FRC會從持久化存儲區(qū)加載數(shù)據(jù),其他地方可以通過FRC獲取數(shù)據(jù)
[fetchedResultController performFetch:&error];
// 錯誤處理
if (error) {
NSLog(@"NSFetchedResultsController init error : %@", error);
}
// 刷新UI
[tableView reloadData];
在上面初始化FRC時,傳入的sectionNameKeyPath:參數(shù),是指明當(dāng)前托管對象的哪個屬性當(dāng)做section的title,在本文中就是Employee表的sectionName字段為section的title。從NSFetchedResultsSectionInfo協(xié)議的indexTitle屬性獲取這個值。
在sectionNameKeyPath:設(shè)置屬性名后,就以這個屬性名作為分組title,相同的title會被分到一個section中。
初始化FRC時參數(shù)managedObjectContext:傳入了一個MOC參數(shù),FRC只能監(jiān)測這個傳入的MOC發(fā)生的本地持久化改變。就像上面介紹時說的,其他MOC對同一個持久化存儲區(qū)發(fā)生的改變,FRC則不能監(jiān)測到這個變化。
再往后面看到cacheName:參數(shù),這個參數(shù)我設(shè)置的是nil。參數(shù)的作用是開啟FRC的緩存,對獲取的數(shù)據(jù)進行緩存并指定一個名字。可以通過調(diào)用deleteCacheWithName:方法手動刪除緩存。
但是這個緩存并沒有必要,緩存是根據(jù)NSFetchRequest對象來匹配的,如果當(dāng)前獲取的數(shù)據(jù)和之前緩存的相匹配則直接拿來用,但是在獲取數(shù)據(jù)時每次獲取的數(shù)據(jù)都可能不同,緩存不能被命中則很難派上用場,而且緩存還占用著內(nèi)存資源。
在FRC初始化完成后,調(diào)用performFetch:方法來同步獲取持久化存儲區(qū)數(shù)據(jù),調(diào)用此方法后FRC保存數(shù)據(jù)的屬性才會有值。獲取到數(shù)據(jù)后,調(diào)用tableView的reloadData方法,會回調(diào)tableView的代理方法,可以在tableView的代理方法中獲取到FRC的數(shù)據(jù)。調(diào)用performFetch:方法第一次獲取到數(shù)據(jù)并不會回調(diào)FRC代理方法。
代理方法
FRC中包含UITableView執(zhí)行過程中需要的相關(guān)數(shù)據(jù),可以通過FRC的sections屬性,獲取一個遵守<NSFetchedResultsSectionInfo>協(xié)議的對象數(shù)組,數(shù)組中的對象就代表一個section。
在這個協(xié)議中有如下定義,可以看出這些屬性和UITableView的執(zhí)行流程是緊密相關(guān)的。
@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
在使用過程中應(yīng)該將FRC和UITableView相互嵌套,在FRC的回調(diào)方法中嵌套UITableView的視圖改變邏輯,在UITableView的回調(diào)中嵌套數(shù)據(jù)更新的邏輯。這樣可以始終保證數(shù)據(jù)和UI的同步,在下面的示例代碼中將會演示FRC和UITableView的相互嵌套。
Table View Delegate
// 通過FRC的sections數(shù)組屬性,獲取所有section的count值
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return fetchedResultController.sections.count;
}
// 通過當(dāng)前section的下標(biāo)從sections數(shù)組中取出對應(yīng)的section對象,并從section對象中獲取所有對象count
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return fetchedResultController.sections[section].numberOfObjects;
}
// FRC根據(jù)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;
}
// 創(chuàng)建FRC對象時,通過sectionNameKeyPath:傳遞進去的section title的屬性名,在這里獲取對應(yīng)的屬性值
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return fetchedResultController.sections[section].indexTitle;
}
// 是否可以編輯
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
// 這里是簡單模擬UI刪除cell后,本地持久化區(qū)數(shù)據(jù)和UI同步的操作。在調(diào)用下面MOC保存上下文方法后,F(xiàn)RC會回調(diào)代理方法并更新UI
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// 刪除托管對象
Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
[context deleteObject:emp];
// 保存上下文環(huán)境,并做錯誤處理
NSError *error = nil;
if (![context save:&error]) {
NSLog(@"tableView delete cell error : %@", error);
}
}
}
上面是UITableView的代理方法,代理方法中嵌套了FRC的數(shù)據(jù)獲取代碼,這樣在刷新視圖時就可以保證使用最新的數(shù)據(jù)。并且在代碼中簡單實現(xiàn)了刪除cell后,通過MOC調(diào)用刪除操作,使本地持久化數(shù)據(jù)和UI保持一致。
就像上面cellForRowAtIndexPath:方法中使用的一樣,FRC提供了兩個方法輕松轉(zhuǎn)換indexPath和NSManagedObject的對象,在實際開發(fā)中這兩個方法非常實用,這也是FRC和UITableView、UICollectionView深度融合的表現(xiàn)。
- (id)objectAtIndexPath:(NSIndexPath *)indexPath;
- (nullable NSIndexPath *)indexPathForObject:(id)object;
Fetched Results Controller Delegate
// Cell數(shù)據(jù)源發(fā)生改變會回調(diào)此方法,例如添加新的托管對象等
- (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數(shù)據(jù)源發(fā)生改變回調(diào)此方法,例如修改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;
}
}
// 本地數(shù)據(jù)源發(fā)生改變,將要開始回調(diào)FRC代理方法。
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
[tableView beginUpdates];
}
// 本地數(shù)據(jù)源發(fā)生改變,F(xiàn)RC代理方法回調(diào)完成。
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
[tableView endUpdates];
}
// 返回section的title,可以在這里對title做進一步處理。這里修改title后,對應(yīng)section的indexTitle屬性會被更新。
- (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName {
return [NSString stringWithFormat:@"sectionName %@", sectionName];
}
上面就是當(dāng)本地持久化數(shù)據(jù)發(fā)生改變后,被回調(diào)的FRC代理方法的實現(xiàn),可以在對應(yīng)的實現(xiàn)中完成自己的代碼邏輯。
在上面的章節(jié)中講到刪除cell后,本地持久化數(shù)據(jù)同步的問題。在刪除cell后在tableView代理方法的回調(diào)中,調(diào)用了MOC的刪除方法,使本地持久化存儲和UI保持同步,并回調(diào)到下面的FRC代理方法中,在代理方法中對UI做刪除操作,這樣一套由UI的改變引發(fā)的刪除流程就完成了。
目前為止已經(jīng)實現(xiàn)了數(shù)據(jù)和UI的雙向同步,即UI發(fā)生改變后本地存儲發(fā)生改變,本地存儲發(fā)生改變后UI也隨之改變??梢酝ㄟ^下面添加數(shù)據(jù)的代碼來測試一下,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中,原有模型文件的基礎(chǔ)上,創(chuàng)建一個新版本的模型文件,然后在此基礎(chǔ)上做不同方式的版本遷移。
本章節(jié)將會講三種不同的版本遷移方案,但都不會講太深,都是從使用的角度講起,可以滿足大多數(shù)版本遷移的需求。
為什么要版本遷移?
在已經(jīng)運行程序并通過模型文件生成數(shù)據(jù)庫后,再對模型文件進行的修改,如果只是修改已有實體屬性的默認值、最大最小值、Fetch Request等屬性自身包含的參數(shù)時,并不會發(fā)生錯誤。如果修改模型文件的結(jié)構(gòu),或修改屬性名、實體名等,造成模型文件的結(jié)構(gòu)發(fā)生改變,這樣再次運行程序就會導(dǎo)致崩潰。
在開發(fā)測試過程中,可以直接將原有程序卸載就可以解決這個問題,但是本地之前存儲的數(shù)據(jù)也會消失。如果是線上程序,就涉及到版本遷移的問題,否則會導(dǎo)致崩潰,并提示如下錯誤:
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.
然而在需求不斷變化的過程中,后續(xù)版本肯定會對原有的模型文件進行修改,這時就需要用到版本遷移的技術(shù),下面開始講版本遷移的方案。
創(chuàng)建新版本模型文件
本文中講的幾種版本遷移方案,在遷移之前都需要對原有的模型文件創(chuàng)建新版本。
選中需要做遷移的模型文件 -> 點擊菜單欄Editor -> Add Model Version -> 選擇基于哪個版本的模型文件(一般都是選擇目前最新的版本),新建模型文件完成。
對于新版本模型文件的命名,我在創(chuàng)建新版本模型文件時,一般會拿當(dāng)前工程版本號當(dāng)做后綴,這樣在模型文件版本比較多的時候,就可以很容易將模型文件版本和工程版本對應(yīng)起來。

添加完成后,會發(fā)現(xiàn)之前的模型文件會變成一個文件夾,里面包含著多個模型文件。

在新建的模型文件中,里面的文件結(jié)構(gòu)和之前的文件結(jié)構(gòu)相同。后續(xù)的修改都應(yīng)該在新的模型文件上,之前的模型文件不要再動了,在修改完模型文件后,記得更新對應(yīng)的模型類文件。
基于新的模型文件,對Employee實體做如下修改,下面的版本遷移也以此為例。

添加一個String類型的屬性,設(shè)置屬性名為sectionName。

此時還應(yīng)該選中模型文件,設(shè)置當(dāng)前模型文件的版本。這里選擇將最新版本設(shè)置為剛才新建的1.1.0版本,模型文件設(shè)置工作完成。
Show The File Inspector -> Model Version -> Current 設(shè)置為最新版本。

對模型文件的設(shè)置已經(jīng)完成了,接下來系統(tǒng)還要知道我們想要怎樣遷移數(shù)據(jù)。在遷移過程中可能會存在多種可能,蘋果將這個靈活性留給了我們完成。剩下要做的就是編寫遷移方案以及細節(jié)的代碼。
輕量級版本遷移
輕量級版本遷移方案非常簡單,大多數(shù)遷移工作都是由系統(tǒng)完成的,只需要告訴系統(tǒng)遷移方式即可。在持久化存儲協(xié)調(diào)器(PSC)初始化對應(yīng)的持久化存儲(NSPersistentStore)對象時,設(shè)置options參數(shù)即可,參數(shù)是一個字典。PSC會根據(jù)傳入的字典,自動推斷版本遷移的過程。
字典中設(shè)置的key:
NSMigratePersistentStoresAutomaticallyOption設(shè)置為YES,CoreData會試著把低版本的持久化存儲區(qū)遷移到最新版本的模型文件。NSInferMappingModelAutomaticallyOption設(shè)置為YES,CoreData會試著以最為合理地方式自動推斷出源模型文件的實體中,某個屬性到底對應(yīng)于目標(biāo)模型文件實體中的哪一個屬性。
版本遷移的設(shè)置是在創(chuàng)建MOC時給PSC設(shè)置的,為了使代碼更直觀,下面只給出發(fā)生變化部分的代碼,其他MOC的初始化代碼都不變。
// 設(shè)置版本遷移方案
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
NSInferMappingModelAutomaticallyOption : @YES};
// 創(chuàng)建持久化存儲協(xié)調(diào)器,并將遷移方案的字典當(dāng)做參數(shù)傳入
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
修改實體名
假設(shè)需要對已存在實體進行改名操作,需要將重命名后的實體Renaming ID,設(shè)置為之前的實體名。下面是Employee實體進行操作。

修改后再使用實體時,應(yīng)該將實體名設(shè)為最新的實體名,這里也就是Employee2,而且數(shù)據(jù)庫中的數(shù)據(jù)也會遷移到Employee2表中。
Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context];
emp.name = @"lxz";
emp.brithday = [NSDate date];
emp.height = @1.9;
[context save:nil];
Mapping Model 遷移方案
輕量級遷移方案只是針對增加和改變實體、屬性這樣的一些簡單操作,假設(shè)有更復(fù)雜的遷移需求,就應(yīng)該使用Xcode提供的遷移模板(Mapping Model)。通過Xcode創(chuàng)建一個后綴為.xcmappingmodel的文件,這個文件是專門用來進行數(shù)據(jù)遷移用的,一些變化關(guān)系也會體現(xiàn)在模板中,看起來非常直觀。
這里還以上面更改實體名,并遷移實體數(shù)據(jù)為例子,將Employee實體遷移到Employee2中。首先將Employee實體改名為Employee2,然后創(chuàng)建Mapping Model文件。
Command + N 新建文件 -> 選擇 Mapping Model -> 選擇源文件 Source Model -> 選擇目標(biāo)文件 Target Model -> 命名 Mapping Model 文件名 -> Create 創(chuàng)建完成。

現(xiàn)在就創(chuàng)建好一個Mapping Model文件,文件中顯示了實體、屬性、Relationships,源文件和目標(biāo)文件之間的關(guān)系。實體命名是EntityToEntity的方式命名的,實體包含的屬性和關(guān)聯(lián)關(guān)系,都會被添加到遷移方案中(Entity Mapping,Attribute Mapping,Relationship Mapping)。
在遷移文件的下方是源文件和目標(biāo)文件的關(guān)系。

在上面圖中改名后的Employee2實體并沒有遷移關(guān)系,由于是改名后的實體,系統(tǒng)還不知道實體應(yīng)該怎樣做遷移。所以選中Mapping Model文件的Employee2 Mappings,可以看到右側(cè)邊欄的Source為invalid value。因為要從Employee實體遷移數(shù)據(jù)過來,所以將其選擇為Employee,遷移關(guān)系就設(shè)置完成了。
設(shè)置完成后,還應(yīng)該將之前EmployeeToEmployee的Mappings刪除,因為這個實體已經(jīng)被Employee2替代,它的Mappings也被Employee2 Mappings所替代,否則會報錯。

在實體的遷移過程中,還可以通過設(shè)置Predicate的方式,來簡單的控制遷移過程。例如只需要遷移一部分指定的數(shù)據(jù),就可以通過Predicate來指定??梢灾苯釉谟覀?cè)Filter Predicate的位置設(shè)置過濾條件,格式是$source.height < 100,$source代表數(shù)據(jù)源的實體。

更復(fù)雜的遷移需求
如果還存在更復(fù)雜的遷移需求,而且上面的遷移方式不能滿足,可以考慮更復(fù)雜的遷移方式。假設(shè)要在遷移過程中,對遷移的數(shù)據(jù)進行更改,這時候上面的遷移方案就不能滿足需求了。
對于上面提到的問題,在Mapping Model文件中選中實體,可以看到Custom Policy這個選項,選項對應(yīng)的是NSEntityMigrationPolicy的子類,可以創(chuàng)建并設(shè)置一個子類,并重寫這個類的方法來控制遷移過程。
- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;
版本遷移總結(jié)
版本遷移在需求的變更中肯定是要發(fā)生的,但是我們應(yīng)該盡量避免這樣的情況發(fā)生。在最開始設(shè)計模型文件數(shù)據(jù)結(jié)構(gòu)的時候,就應(yīng)該設(shè)計一個比較完善并且容易應(yīng)對變化的結(jié)構(gòu),這樣后面就算發(fā)生變化也不會對結(jié)構(gòu)主體造成大的改動。
好多同學(xué)都問我有Demo沒有,其實文章中貼出的代碼組合起來就是個Demo。后來想了想,還是給本系列文章配了一個簡單的Demo,方便大家運行調(diào)試,后續(xù)會給所有博客的文章都加上Demo。
Demo只是來輔助讀者更好的理解文章中的內(nèi)容,應(yīng)該博客結(jié)合Demo一起學(xué)習(xí),只看Demo還是不能理解更深層的原理。Demo中幾乎每一行代碼都會有注釋,各位可以打斷點跟著Demo執(zhí)行流程走一遍,看看各個階段變量的值。
Demo地址:劉小壯的Github
這兩天更新了一下文章,將CoreData系列的六篇文章整合在一起,做了一個PDF版的《CoreData Book》,放在我Github上了。PDF上有文章目錄,方便閱讀。
如果你覺得不錯,請把PDF幫忙轉(zhuǎn)到其他群里,或者你的朋友,讓更多的人了解CoreData,衷心感謝!??
