Understanding coredata – advanced usage

Time:2020-2-12
This article belongs to the original of “Jianshu Liu Xiaozhuang”. Please indicate:

< Jianshu Liu Xiaozhuang > http://www.jianshu.com/p/01f36026da7d


In the previous articles, I have talked a lot aboutCoreDataUse relevant knowledge points. This article focuses on two aspects,NSFetchedResultsControllerAnd version migration.

Although there are“Advanced”In fact, the two words are not advanced, just because there are too many things in the last article, two more complex knowledge points are moved to this article. A kind of

If there is any omission or mistake in the article, please put forward it in time, thank you! A kind of


Understanding coredata - advanced usage

NSFetchedResultsController

Often used in the development processUITableViewThese view classesNeed to manage their own data sources, including network access, local storage need to write code for management.

While inCoreDataProvided inNSFetchedResultsControllerClass (fetched results controllerAlso calledFRC),FRCCan manageUITableVieworUICollectionViewData source for. This data source mainlyLocal persistent dataThis data source can also be used together with the network request data, mainly depending on the business requirements.

This article will useUITableViewAs a view class, withNSFetchedResultsControllerFor the next demonstration,UICollectionViewCoordinationNSFetchedResultsControllerThe use of is similar, not all of which are mentioned here.

Brief introduction

As mentioned above,NSFetchedResultsControllerIt’s like the two aboveData manager for viewsSame.FRCCan monitor aMOCIfMOCWhen the managed object is added, deleted or modified, the local persistent data will be changed,FRCThe corresponding proxy method will be called back, and the parameters of the callback method will include the type of operation to be performed, the value of the operationindexPathEqual parameters.

In actual use, throughFRCBindingOneMOCWillUITableViewEmbedded inFRCIn the execution process of. Anywhere, for thisBindingOfMOCIf the storage area is modified, it will triggerFRCThe callback method ofFRCEmbedded in callback method ofUITableViewCode and make corresponding modification.

From this we can see thatFRCThe biggest advantage is,Always consistent with local persistent data。 As long as the local persistent data changes, it will triggerFRCTo update the upper data source andUI。 In this way, simply speaking, it can be calledData driven UI

Understanding coredata - advanced usage

But it’s important to note thatFRCAnMOCParameters,FRCOnly incoming can be monitoredMOCWhat happened. Suppose otherMOCThe same store has changed,FRCIt can’t monitor the change and will not respond.

So useFRCWe need to pay attention toFRCOnly oneMOCWe respond to changes inCoreDataIn the design of persistence layer, one storage area should only correspond to oneMOC, or set a responsibleUIOfMOC, which will be explained in detail in the multithreading section later.

Modify model file structure

Before writing the code, make some changes to the previous model file structure.

Understanding coredata - advanced usage

speakFRCWhen it’s time to useEmployeeThis table, other tables and settings are ignored directly. Need toEmployeeBased on the original field, add aStringTypesectionNameField, which is used to storesection title, which will be discussed in detail in the following article.

Initialize FRC

The following example is more commonly usedFRCInitialization method, specified during initializationMOCAnd I’ll use what I said beforeMOCInitialization code,UITableViewThe initialization code is also omitted here, which is mainly highlightedFRCInitialization of.

//Create the request object and indicate the operation employee table
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"Employee"];
//Set the sorting rules, indicating to sort in ascending order according to the height field
NSSortDescriptor *heightSort = [NSSortDescriptor sortDescriptorWithKey:@"height" ascending:YES];
request.sortDescriptors = @[heightSort];

//Create nsfetchedresultscontroller instance and bind MOC
NSError *error = nil;
fetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:request 
                                                              managedObjectContext:context 
                                                                sectionNameKeyPath:@"sectionName" 
                                                                         cacheName:nil];
//Set up the agent and comply with the agreement
fetchedResultController.delegate = self;
//Execute the get request. After execution, FRC will load data from the persistent store, and other places can get data through FRC
[fetchedResultController performFetch:&error];

//Error handling
if (error) {
    NSLog(@"NSFetchedResultsController init error : %@", error);
}

// refresh UI
[tableView reloadData];

Initialize onFRCWhen, incomingsectionNameKeyPath:Parameter, which property of the current managed object is used assectionOftitle, in this articleEmployeeTablesectionNameField issectionOftitle。 fromNSFetchedResultsSectionInfoAgreementindexTitleProperty gets the value.

staysectionNameKeyPath:After setting the property name, group the property nametitleThe sametitleWill be divided into onesectionMedium.

InitializationFRCTime parametermanagedObjectContext:Passed in aMOCParameters,FRCOnly this incomingMOCLocal persistence changes that occur. As mentioned above, othersMOCChanges to the same persistent store,FRCThe change could not be detected.

Look backcacheName:Parameter, this parameter I set isnil。 The function of the parameter is to enableFRCTo cache the acquired data and specify a name. You can calldeleteCacheWithName:Method to manually delete the cache.

But this cache is not necessary. The cache is based onNSFetchRequestObject to match. If the currently acquired data matches the previously cached data, it will be used directly. However, the data acquired each time may be different,Cache cannot be hitIt’s hard to use, and caching alsoOccupied memory resources

stayFRCAfter initialization, callperformFetch:Method to get persistent store data synchronously. After calling this methodFRCThe property that holds the data has a value. After getting the data, calltableViewOfreloadDataMethod, call backtableViewThe proxy method oftableViewGet theFRCData. callperformFetch:Method gets data for the first time and does not call backFRCProxy method.

Agent method

FRCIncludeUITableViewThe relevant data needed in the execution process can be obtained throughFRCOfsectionsProperty, get a compliance<NSFetchedResultsSectionInfo>The object array of the protocol, in which the object represents asection

In this protocol, there are the following definitions. You can see that these attributes andUITableViewThe execution process of is closely related.

@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

In the process of use, theFRCandUITableViewNested inFRCNested in callback methods ofUITableViewView changes logic inUITableViewThe logic of data update is nested in the callback of. This wayKeep data and UI synchronized at all times, which will be shown in the following sample codeFRCandUITableViewAre nested with each other.

Table View Delegate
//Get the count value of all sections through the sections array property of FRC
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return fetchedResultController.sections.count;
}

//Get the corresponding section object from the sections array through the subscript of the current section, and get all the objects count from the section object
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return fetchedResultController.sections[section].numberOfObjects;
}

//FRC gets the managed object according to indexpath and assigns a value to the 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;
}

//When creating FRC object, the attribute name of section title passed in through sectionnamekeypath: is used to get the corresponding attribute value here
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
    return fetchedResultController.sections[section].indexTitle;
}

//Can I edit
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
    return YES;
}

//Here is a simple simulation of the operation of synchronizing the data in the local persistence area with the UI after the cell is deleted in the UI. After calling the following MOC to save the context method, FRC will call back the proxy method and update the UI
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
     if (editingStyle == UITableViewCellEditingStyleDelete) {
        //Delete managed objects
        Employee *emp = [fetchedResultController objectAtIndexPath:indexPath];
        [context deleteObject:emp];
        //Save context and do error handling
        NSError *error = nil;
        if (![context save:&error]) {
            NSLog(@"tableView delete cell error : %@", error);
        }
    }
}

Above isUITableViewThe proxy method is nested in the proxy methodFRCData acquisition code, so that when you refresh the view, you can ensure that the latest data is used. And in the code simple implementation of deletioncellAfter passingMOCCall delete operation to make local persistent data andUIbring into correspondence with.

Just like abovecellForRowAtIndexPath:Method,FRCProvides two easy ways to switchindexPathandNSManagedObjectThese two methods are very practical in the actual development, which is alsoFRCandUITableViewUICollectionViewPerformance of deep integration.

- (id)objectAtIndexPath:(NSIndexPath *)indexPath;
- (nullable NSIndexPath *)indexPathForObject:(id)object;

Fetched Results Controller Delegate
//This method will be called back when the cell data source changes, such as adding a new managed object, etc
- (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;
    }
}

//Call back this method when the section data source changes, such as modifying the 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;
    }
}

//The local data source has changed and a callback to the FRC agent method will begin.
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
    [tableView beginUpdates];
}

//When the local data source changes, the FRC proxy method callback is completed.
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller {
    [tableView endUpdates];
}

//Return the title of the section. You can do further processing on the title here. After the title is modified here, the indextitle property of the corresponding section will be updated.
- (nullable NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName {
    return [NSString stringWithFormat:@"sectionName %@", sectionName];
}

It’s a pawnLocal persistent data changesAfter the callbackFRCThe implementation of agent method can complete its own code logic in the corresponding implementation.

Deletion is mentioned in the previous sectioncellAfter the local persistent data synchronization problem. DeletecellLater ontableViewIn the callback of the proxy method, theMOCFor local persistent storage andUIKeep syncing and call back to the followingFRCIn the proxy method, in the proxy methodUIDo delete operation, such a setTriggered by UI changesThe deletion process of is completed.

Data andUIOfBidirectional synchronizationThat is to sayUIThe local storage changes after the change, and the local storage changes after the changeUIIt also changes. You can test it by adding data code below,NSFetchedResultsControllerThat’s all.

- (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);
    }
}    

Version migration

CoreDataThere are many ways to migrate versions, usually firstXcodeCreate a new version of the model file based on the original model file, and then migrate the version in different ways.

This chapter will talk about three different version migration schemes, but none of them will go too far. From the perspective of use, they can meet the needs of most version migration.

Why version migration?

After the program has been run and the database has been generated from the model file, the modification of the model file can only be performed by modifying the default value, maximum and minimum value of the existing entity propertyFetch RequestWhen the property itself contains parameters, no errors occur. If you modify the structure of the model file, or modify the attribute name, entity name, etc., theThe structure of the model file changesRun the program againIt will lead to collapse

In the development and test process, you can directly uninstall the original program to solve this problem, butData stored locally will also disappear。 If it is an online program, it involves version migration. Otherwise, it will crash, and the following error will be prompted:

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.

However, in the process of changing requirements, subsequent versions will definitely modify the original model files. At this time, version migration technology is needed. Next, we will talk about the version migration scheme.

Create a new version of the model file

Several version migration schemes mentioned in this article need to create new versions of the original model files before migration.

Select the model file to be migrated - > click the menu bar editor - > add model version - > select the model file based on which version (generally the latest version is selected), and the new model file is completed.

For the naming of new version model files, when I create new version model files, I usuallyTake the current project version number as the suffixIn this way, when there are many versions of model files, it is easyMatch model file version with project version

Understanding coredata - advanced usage

After adding, it will be found that the previous model file will become a folder, which contains multiple model files.

Understanding coredata - advanced usage

In the new model file, the file structure is the same as the previous file structure. Subsequent changes should be made to the new model file. The previous model file should not be moved any more. After modifying the model file, rememberUpdate the corresponding model class file

Based on the new model file, theEmployeeThe entity is modified as follows, and the following version migration is also used as an example.

Understanding coredata - advanced usage

Add oneStringProperty of type, set the property name tosectionName

Understanding coredata - advanced usage

At this time, you should also select the model file and set the version of the current model file. Select here to set the latest version to the new one just created1.1.0 version, the model file setting is completed.

Show the file Inspector - > model version - > current is set to the latest version.

Understanding coredata - advanced usage

The setup of the model file is complete. NextThe system also needs to know how we want to migrate data。 There may be many possibilities in the migration process, and apple has left this flexibility for us to complete. The rest is to write the migration scheme and detailed code.

Lightweight version migration

The lightweight version migration scheme is very simple, most of the migration work is done by the system, just tell the system how to migrate. In persistent storage coordinator(PSC)Initialize the corresponding persistent storage(NSPersistentStore)Objects, settingoptionsParameter is a dictionary.PSCThe version migration process is automatically inferred based on the incoming dictionary.

Set in dictionarykey
  • NSMigratePersistentStoresAutomaticallyOptionSet toYESCoreDataYou will try to migrate the low version of the persistence store to the latest version of the model file.
  • NSInferMappingModelAutomaticallyOptionSet toYESCoreDataWe will try to automatically infer which attribute in the entity of the source model file corresponds to in the entity of the target model file in the most reasonable way.

Version migration settings are created inMOCGive time toPSCSet, in order to make the code more intuitive, only the changed part of the code is shown below, othersMOCThe initialization code for is unchanged.

//Set version migration scheme
NSDictionary *options = @{NSMigratePersistentStoresAutomaticallyOption : @YES,
                                NSInferMappingModelAutomaticallyOption : @YES};

//Create a persistent storage coordinator and pass in the dictionary of the migration scheme as a parameter
[coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[NSURL fileURLWithPath:dataPath] options:options error:nil];
Modify entity name

Suppose you need to rename an existing entity, and you need to rename the entityRenaming ID, set to the previous entity name. Below isEmployeeEntity.

Understanding coredata - advanced usage

When using an entity after modification, you should set the entity name to the latest entity name, which isEmployee2, and the data in the database will also be migrated toEmployee2In the table.

Employee2 *emp = [NSEntityDescription insertNewObjectForEntityForName:@"Employee2" inManagedObjectContext:context];
emp.name = @"lxz";
emp.brithday = [NSDate date];
emp.height = @1.9;
[context save:nil];

Mapping model migration scheme

The lightweight migration scheme is only for simple operations such as adding and changing entities and attributes. If there are more complex migration requirements, it should be usedXcodeMigration template provided(Mapping Model) adoptXcodeCreate a suffix of.xcmappingmodelThis file is specially used for data migration. Some changes will also be reflected in the template,It looks very intuitive

In this case, you can change the entity name and migrate the entity dataEmployeeEntity migration toEmployee2Medium. First of allEmployeeEntity renamedEmployee2, and createMapping ModelPapers.

Command + n create new file - > select mapping model - > select source file source model - > select target file target model - > name mapping model file name - > create finished.

Understanding coredata - advanced usage

Create one nowMapping ModelFile, showing entities, propertiesRelationships, relationship between source and target files. Entity name isEntityToEntityThe attributes and association relationships contained in the entity will be added to the migration scheme(Entity MappingAttribute MappingRelationship Mapping)。

Below the migration file is the relationship between the source and destination files.

Understanding coredata - advanced usage

After the change of name in the picture aboveEmployee2The entity has no migration relationship. Because it is a renamed entity, the system does not know how to migrate the entity. So selectMapping ModelDocumentationEmployee2 Mappings, you can see theSourcebyinvalid value。 Because fromEmployeeThe entity migrates the data, so select it asEmployee, the migration relationship is set.

After setting up, you should alsoEmployeeToEmployeeOfMappingsDelete because this entity has beenEmployee2Instead, it’sMappingsAlso beEmployee2 MappingsReplaced by, otherwise an error will be reported.

Understanding coredata - advanced usage

During entity migration, you can also setPredicateTo simply control the migration process. For example, you can migrate only a part of the specified data throughPredicateTo specify. It can be directly on the right sideFilter PredicateSet the filter condition in the format of$source.height < 100$sourceThe entity that represents the data source.

Understanding coredata - advanced usage

More complex migration requirements

If there are still more complex migration requirements, and the above migration methods cannot be met, more complex migration methods can be considered. If you want to change the migrated data during the migration process, the above migration scheme cannot meet the requirements.

For the above problems, theMapping ModelSelect the entity in the file, and you can seeCustom PolicyThis option corresponds toNSEntityMigrationPolicyYou can create and set a subclass and override its methods to control the migration process.

- (BOOL)createDestinationInstancesForSourceInstance:(NSManagedObject *)sInstance entityMapping:(NSEntityMapping *)mapping manager:(NSMigrationManager *)manager error:(NSError **)error;

Version migration summary

Version migration is bound to happen in the change of requirements, but we should try to avoid such a situation. When you first design the data structure of the model file, you shouldDesign a relatively perfect structure that is easy to cope with changesIn this way, even if there is a change, it will not make a big change to the main body of the structure.


Many students asked me that I hadDemoNo, in fact, the code posted in the article is a combination ofDemo。 After thinking about it, I still gave this series a simpleDemo, which is convenient for you to run and debug. In the future, all blog articles will be addedDemo

DemoJust to help readers better understand the content of the article,Should blog combineDemoStudy together, just watchDemoStill can’t understand the deeper principleDemoAlmost every line of code in will have a comment. You can break it and followDemoGo through the execution process to see the values of variables in each phase.

Demo address: Liu Xiaozhuang’s GitHub


The article has been updated in the past two daysCoreDataThe six articles in the series are integrated into onePDFVersion of coredata book, on my GitHub.PDFThere is a list of articles on it for easy reading.

If you think it’s good, please transfer PDF to other groups or your friends to let more people know coredata. Thank you!😁

Understanding coredata - advanced usage