Practical part
As Max Mikheyenko commented quite correctly, the FRC receives the objects after -performFetch .
However, in order to obtain the number of objects once, the use of FRC is superfluous. It is enough to do, for example, like this:
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"TMEmployee"]; request.predicate = [NSPredicate predicateWithFormat:@"kind == %@", @(TMEmployeeKind)]; NSError *error; NSUInteger count = [context countForFetchRequest:request error:&error];
The predicate does not need to be specified if you need to get the number of all objects of the TMEmployee entity.
I also want to note that sortDescriptors are not needed here for obvious reasons.
Theoretical part
Outline the core ideology of Core Data .
What is it all about?
The task performed by Core Data is the application data management. Here the library gives us the opportunity to balance between several conflicting requirements:
- Quick access to data.
- Convenience of work.
- Low memory profile.
The unofficial definition of Core Data is an object graph management system with dancers and chess. That is, the system holds objects and connections between them, and provides pleasant utilities such as lazy loading of data and automatic notification of changes in objects. Full list of features here .
Why it is worth using
In short, because if we tried to base our data model on pure SQLite , for example, with further scaling of the application and increasing the amount of data, we will eventually find out that we are writing our Core Data . And it already solved, for example:
- Faulting and Uniquing . By default, when the query is executed, the object is loaded into the context as a lightweight "stub". When accessing any of its properties,
Core Data fills this object with data. Such a stub is called a fault , and the process of loading data into it is faulting . Actually, a fault is simply a possible state of an object of the NSManagedObject class. He, by the way, is unique. Each request to the context, the results of which contain some specific object, will return a pointer to the same instance of the object - thus avoiding duplication of data in memory. - Batching The query results can be loaded in batches, for 50 pieces, for example. It is very convenient for large data sets, about which it is known that they will be displayed sequentially, as, for example, when scrolling a table. When you have to show the 51st line, and the
UITableView delegate will -tableView:cellForRowAtIndexPath: method, the delegate will execute DBPerson *person = [self.frc objectAtIndexPath:indexPath]; to retrieve data from the 51st object DBPerson *person = [self.frc objectAtIndexPath:indexPath]; , and Core Data itself will request the next package. Enabled by one line request.fetchBatchSize = 50; . Convenient access to properties and connections of objects. After generating the entity classes (Editor / Create NSManagedObject subclass ...), the properties of Core Data objects can be accessed in the usual way:
DBDepartment *department = [self.frc objectAtIndexPath:indexPath]; DBPerson *person = [NSEntityDescription insertNewObjectForEntityForName:@"DBPerson" inManagedObjectContext:context]; person.nameLast = @"ΠΠΎΠ±ΡΠΈΠ½ΡΠΊΠΈΠΉ"; person.nameFirst = @"ΠΡΡΡ"; person.nameMiddle = @"ΠΠ²Π°Π½ΠΎΠ²ΠΈΡ"; person.department = department;
The library has a rather long history, rooted in EOF NeXT, and has been very well tested in its more than ten-year history. That does not exclude, of course, the presence of almost as ancient glitches in it .
What is a "stack"
This is a collection of objects that control the data layer in Core Data . For example, like this:
[NSPersistentStore] β [NSPersistentStoreCoordinator] β [NSManagedObjectContext] β β [SQLite file] [NSManagedObjectModel]
The best-known example of a stack can be viewed by creating a new project in Xcode with the "Use Core Data" AppDelegate on, it will be in the AppDelegate class.
Context
The application interacts with the data through NSManagedObjectContext . In the most common use case, mainContext is the current state of the application data, what the interface is attached to, what the user sees. This data can be changed by adding or deleting objects, changing their properties and establishing or breaking links between them in accordance with user actions, or, for example, when receiving data from a web service. All these changes are in the context memory and can be synchronously displayed in the interface. If necessary, these changes can be saved so that the current state of the data survives the termination of application execution or device shutdown. The content of the context is not necessarily equivalent to the complete data graph of the application. Only those objects that are directly or indirectly requested using the FRC , -executeFetchRequest:error: -objectWithID: etc. are -objectWithID: . NSManagedObjectContext objects are very cheap to create, they should not be afraid to create and release for any intermediate calculations.
The data source for the context can be either the NSPersistentStoreCoordinator , or the parent context.
Model
NSManagedObjectModel describes the data structure of an application: the types of objects, their properties, and their relationships. It may also contain some other information - configurations and model versions. Usually, a model is created visually, in the Xcodov Core Data Model Editor, but it can also be programmed.
Storage
NSPersistentStore is responsible for the physical storage of data. Established there are four types: SQLite , Binary , XML and In-Memory . Most of them will use SQLite . Technically, you can create your own storage class that receives data from a web service, connect this storage to the Core Data stack, and work with it as you would a regular SQLite file. And such attempts were.
Coordinator
NSPersistentStoreCoordinator binds the repository (or several repositories) to the data model and issues and stores data on the requests of the associated contexts NSManagedObjectContext , organizing these requests into a queue.
In iOS 10, the coordinator supports parallel execution of several reading tasks plus one writing task. In systems up to the ninth, it sometimes makes sense to keep several coordinators connected to the same SQLite file (see the section on large imports below).
Example
I give a simple code for initializing the stack in the form of a singleton and the connection of the main and background contexts:
DataSource.h
@interface DataSource : NSObject @property (nonatomic, readonly) NSPersistentStoreCoordinator *coordinator; @property (nonatomic, readonly) NSManagedObjectContext *mainContext; @property (nonatomic, readonly) NSManagedObjectContext *backgroundContext; + (instancetype)shared; @end
DataSource.m
@interface DataSource () @property (nonatomic, strong) NSPersistentStoreCoordinator *coordinator; @property (nonatomic, strong) NSManagedObjectContext *mainContext; @property (nonatomic, strong) NSManagedObjectContext *backgroundContext; @end @implementation DataSource + (instancetype)shared { static dispatch_once_t onceToken; static id _singleton; dispatch_once(&onceToken, ^{ _singleton = [[self alloc] initInternal]; }); return _singleton; } - (instancetype)init { NSLog(@"ΠΠΈΠΊΠ°ΠΊΠΈΡ
alloc] init]! ΠΡΠΏΠΎΠ»ΡΠ·ΡΠΉΡΠ΅ singleton [DataSource shared]."); abort(); return nil; } - (instancetype)initInternal { self = [super init]; if (!self) { return nil; } // ΡΡΡΠΊ Core Data NSURL *modelURL = [[NSBundle mainBundle] URLForResource:@"Model" withExtension:@"momd"]; NSManagedObjectModel *model = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL]; self.coordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:model]; NSURL *documentsURL = [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; NSURL *storeURL = [documentsURL URLByAppendingPathComponent:@"Data.sqlite"]; NSError *error = nil; NSDictionary *options = @{ NSMigratePersistentStoresAutomaticallyOption: @YES, NSInferMappingModelAutomaticallyOption: @YES, }; NSPersistentStore *store = [self.coordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]; if (!store) { error = [NSError errorWithDomain:@"MY-OWN-ERROR-DOMAIN" code:9999 userInfo:@{ NSLocalizedDescriptionKey: @"ΠΠ΅ ΡΠ΄Π°Π»ΠΎΡΡ ΠΏΠΎΠ΄ΠΊΠ»ΡΡΠΈΡΡ Ρ
ΡΠ°Π½ΠΈΠ»ΠΈΡΠ΅ Core Data", NSUnderlyingErrorKey: error, }]; NSLog(@"Unresolved error %@, %@", error, [error userInfo]); abort(); } self.mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType]; self.mainContext.persistentStoreCoordinator = self.coordinator; self.backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; self.backgroundContext.persistentStoreCoordinator = self.coordinator; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.mainContext]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(backgroundContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:self.backgroundContext]; return self; } - (void)mainContextDidSave:(NSNotification *)notification { [self.backgroundContext performBlock:^{ [self.backgroundContext mergeChangesFromContextDidSaveNotification:notification]; }]; } - (void)backgroundContextDidSave:(NSNotification *)notification { [self.mainContext performBlock:^{ [self.mainContext mergeChangesFromContextDidSaveNotification:notification]; }]; } @end
How to get data
After the stack is assembled, you can query it for existing data, create new objects and save changes.
NSFetchRequest is a context call describing the type of objects requested, filtering criteria, sorting order, and still quite a significant amount of much less widely known parameters.
At the NSPersistentStore SQLite level, for example, of a type, the query is translated into SQL commands and passed to the SQLite library.
You can use the query in several ways. From common:
- Ask the context to execute this query with the
-executeFetchRequest:error: method -executeFetchRequest:error: The result will be an array. In the simplest case, with the default resultType == NSManagedObjectResultType , this array will contain NSManagedObject objects. - Ask the context to count the number of objects for this request using the
-countForFetchRequest:error: method. It is executed significantly faster due to the fact that property values ββare not loaded, and NSManagedObject objects are not created. - Send this request to
NSFetchedResultsController . After performing performFetch: FRC will receive objects matching this request. If you also specify a delegate for the FRC, it will continuously monitor changes in the NSManagedObjectContext context, and notify the delegate via the NSFetchedResultsControllerDelegate protocol about the changes in the state of the objects.
Items 1 and 2 are useful for some one-time actions.
Point 3 is used to permanently link the state of the data with the interface, UITableView , for example.
How to count the units
For example:
- (void)calculateAggregateValues { NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType]; context.persistentStoreCoordinator = [[DataSource shared] coordinator]; [context performBlock:^{ // Π½Π°ΠΈΠΌΠ΅Π½ΠΎΠ²Π°Π½ΠΈΠ΅ ΠΏΠΎΡΡΠ°Π²ΡΠΈΠΊΠ° NSExpressionDescription *nameDescription = [[NSExpressionDescription alloc] init]; nameDescription.expression = [NSExpression expressionForKeyPath:@"supplierName"]; nameDescription.name = @"name"; nameDescription.expressionResultType = NSStringAttributeType; // ΠΊΠΎΠ»ΠΈΡΠ΅ΡΡΠ²ΠΎ ΠΊΠΎΠ½ΡΡΠ°ΠΊΡΠΎΠ² NSExpressionDescription *countDescription = [[NSExpressionDescription alloc] init]; countDescription.expression = [NSExpression expressionForKeyPath:@"sumPlanned.@count"]; countDescription.name = @"totalCount"; countDescription.expressionResultType = NSInteger32AttributeType; // ΠΏΠ»Π°Π½ΠΈΡΡΠ΅ΠΌΠ°Ρ ΡΡΠΌΠΌΠ° NSExpressionDescription *sumDescription = [[NSExpressionDescription alloc] init]; sumDescription.expression = [NSExpression expressionForKeyPath:@"@sum.sumPlanned"]; sumDescription.name = @"totalSum"; sumDescription.expressionResultType = NSDoubleAttributeType; // Π·Π°ΠΏΡΠΎΡ NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:@"DBContract"]; request.resultType = NSDictionaryResultType; request.predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[ [NSPredicate predicateWithFormat:@"datePlanned < %@", [NSDate date]], ]]; request.propertiesToFetch = @[nameDescription, countDescription, sumDescription]; request.propertiesToGroupBy = @[nameDescription]; // Π²ΡΠΏΠΎΠ»Π½ΠΈΠΌ Π·Π°ΠΏΡΠΎΡ NSArray *result = [context executeFetchRequest:request error:nil]; // Π²ΠΎΠ·ΡΠΌΡΠΌ ΠΎΠ±ΡΠ΅ΠΊΡ Ρ ΡΠ°ΠΌΠΎΠΉ ΠΊΡΡΠΏΠ½ΠΎΠΉ ΡΡΠΌΠΌΠΎΠΉ NSDictionary *values = [[result sortedArrayUsingDescriptors:@[ [NSSortDescriptor sortDescriptorWithKey:@"totalSum" ascending:NO], ]] firstObject]; // ΠΏΠΎΠΊΠ°ΠΆΠ΅ΠΌ ΡΠ΅Π·ΡΠ»ΡΡΠ°ΡΡ Π² ΠΈΠ½ΡΠ΅ΡΡΠ΅ΠΉΡΠ΅ dispatch_async(dispatch_get_main_queue(), ^{ self.nameLabel.text = values[@"name"]; self.countLabel.text = [values[@"totalCount"] stringValue]; self.sumLabel.text = [values[@"totalSum"] stringValue]; }); }]; }
Bulky, but it works smartly. You can read, for example, here or in the documentation .
Why does the application need multiple contexts?
To unload the main thread. As soon as a difficult task appears, and the interface begins to slow down - the task must be led off to the background. For the case of counting aggregates, for example, an NSManagedObjectContext type NSPrivateQueueConcurrencyType , in its -performBlock: the task is executed, and the results are transferred to the interface in main thread via dispatch_async (see example above).
The case of large data import will be discussed below.
How to respond to changes in data
The task of the application is usually to synchronize the interface with the state of the data with as little delay as possible. NSFetchedResultsController specially created for this task: it tracks changes in a given set of context data and notifies its delegate ( UIViewController , for example) about them, which, upon receiving these notifications, updates the interface accordingly. The classic NSFetchedResultsController with UITableView is here .
The alternative to FRC is to subscribe to the NSManagedObjectContextDidChangeNotification notification, and independently parse the resulting NSNotification object.
How not to let the interface slow down
Very simple: leave in it only the data that the user sees, and not load what the user may never see.
For an interface, one common NSManagedObjectContext with the NSMainQueueConcurrencyType type is most often held β specifically designed to work in main thread. All NSManagedObject , the data from which will go to all sorts of UIlabel , UICollectionView , UITableView , etc. objects of interface classes must be generated by such a context, otherwise you will have to mess around with the fact that you can access the properties of NSManagedObject objects only in the stream of their context, and interface elements only in main thread.
Do not load unnecessary data - it is solved by faulting, batching and estimatedRowHeight methods for the case with different table heights. For example, in the data table - two thousand records. It does not make sense to load them all at once, as the user will see thirty lines at most on the first display of the table. The data of the remaining one thousand nine hundred and seventy objects will be loaded in vain, as the user immediately climbs into the search, and the data set is re-requested from Core Data with a significant reduction by the predicate, which takes into account the user input string. If UITableView not provided with the expected row height, then after receiving the number of sections and objects in sections from the delegate, the table will need to determine the overall height of its content, and it will rush to calculate (in the case of auto layout in rows) or require a delegate (in the case of -tableView:heightForRowAtIndexPath: heights of each of two thousand lines. In both cases, this will result in a line height calculation for the specified widths and attributes using the -boundingRectWithSize:options:attributes:context: method -boundingRectWithSize:options:attributes:context: - and this is an extremely expensive method that runs on main thread. On current devices, the interface will hang for about five seconds, or even more, depending on the complexity of the cells.
How to make custom changes
Typically, the user generates a surprisingly small change in the data. Of course, situations like "delete all records obtained from TSB" are possible. But more often, user activity is limited by insanely long-term text bobbing (this changes the single text property of the DBMessage object), tapes on the like button (one DBPerson object is added to the DBMessage connection of the DBMessage object) and DBPerson button (a modified object saved, in the background is serialized and passed to the web service API).
Such a trifle can and should be done directly in mainContext .
Creating and editing objects is conveniently done on a child context connected to mainContext and also of type NSMainQueueConcurrencyType . For example, an employee's account card opens a UIViewController UIViewController in the popover that creates a child context, and in it loads the specified or creates a new DBPerson object, the data of which is scattered over the controller's views for viewing and editing. The user makes his changes, saves "Save", the child context is sent -save: changes are pushed into mainContext , which can also be immediately saved so that the data in the NSManagedObjectContext -> NSPersistentStoreCoordinator -> NSPersistentStore would fall into the SQLite file.
If the user drops "Cancel" - you can simply remove the controller without saving the context. In this case, the changes will simply disappear with him.
How to make large imports
Slight data changes can be made directly to mainContext objects. But it makes sense to use background contexts ( NSPrivateQueueConcurrencyType ) to make large-scale changes to the data.
The following scheme proved to be viable:
Π§ΡΠΎ Ρ ΠΊΡΡΠΈΡΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ
Core Data ΠΊΡΡΠΈΡΡΠ΅Ρ Π΄Π°Π½Π½ΡΠ΅ Π½Π° Π½Π΅ΡΠΊΠΎΠ»ΡΠΊΠΈΡ
ΡΡΠΎΠ²Π½ΡΡ
.
NSManagedObjectContext , Π½Π°ΠΏΡΠΈΠΌΠ΅Ρ, Π² ΠΎΡΠ²Π΅Ρ Π½Π° Π·Π°ΠΏΡΠΎΡ Π²Π΅ΡΠ½ΡΡ ΠΎΠ±ΡΠ΅ΠΊΡΡ, Π½Π΅ ΠΎΠ±ΡΠ°ΡΠ°ΡΡΡ ΠΊ ΡΠ²ΠΎΠ΅ΠΌΡ ΠΊΠΎΠΎΡΠ΄ΠΈΠ½Π°ΡΠΎΡΡ, Π΅ΡΠ»ΠΈ ΡΡΠΈ ΠΎΠ±ΡΠ΅ΠΊΡΡ ΡΠΆΠ΅ ΡΠ°Π½Π΅Π΅ Π±ΡΠ»ΠΈ ΠΈΠΌ ΠΏΠΎΠ»ΡΡΠ΅Π½Ρ.NSPersistentStoreCoordinator Π΄Π΅ΡΠΆΠΈΡ row cache β ΡΡΡΡΠ΅ Π΄Π°Π½Π½ΡΠ΅, ΠΏΠΎΠ»ΡΡΠ΅Π½Π½ΡΠ΅ ΠΎΡ SQLite , ΠΈ ΠΏΡΠΈ ΠΎΠ±ΡΠ°ΡΠ΅Π½ΠΈΠΈ ΠΊ Π½Π΅ΠΌΡ ΡΠ°Π·Π½ΡΡ
ΠΊΠΎΠ½ΡΠ΅ΠΊΡΡΠΎΠ² Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅Ρ Π΄Π°Π½Π½ΡΠ΅ ΠΈΠΌΠ΅Π½Π½ΠΎ ΠΈΠ· ΡΡΠΎΠ³ΠΎ ΠΊΡΡΠ°, Π½Π΅ ΠΎΠ±ΡΠ°ΡΠ°ΡΡΡ ΠΊ SQLite .- ΠΡ ΠΈ ΠΏΠ»ΡΡ Ρ ΡΠ°ΠΌΠΎΠΉ Π±ΠΈΠ±Π»ΠΈΠΎΡΠ΅ΠΊΠΈ
SQLite Π΅ΡΡΡ ΡΠ²ΠΎΠΉ ΡΠΎΠ±ΡΡΠ²Π΅Π½Π½ΡΠΉ ΠΊΡΡ.
ΠΠ°ΠΊ ΠΎΡΠ»Π°ΠΆΠΈΠ²Π°ΡΡ
Product / Scheme / Edit Scheme / Run / Arguments. Π Arguments Passed On Launch Π΄ΠΎΠ±Π°Π²ΠΈΡΡ:
-com.apple.CoreData.SQLDebug 1 β Π²ΠΊΠ»ΡΡΠΈΡ Π»ΠΎΠ³ Π·Π°ΠΏΡΠΎΡΠΎΠ² Core Data ΠΊ SQLite .-com.apple.CoreData.ConcurrencyDebug 1 β Π±ΡΠ΄Π΅Ρ Π²ΡΠΊΠΈΠ΄ΡΠ²Π°ΡΡ exception ΠΏΡΠΈ ΠΏΠΎΠΏΡΡΠΊΠ΅ Π΄ΠΎΡΡΡΠΏΠ° ΠΊ ΡΠ²ΠΎΠΉΡΡΠ²Π°ΠΌ NSManagedObject ΠΈΠ· Π½Π΅ ΡΠΎΠ΄Π½ΠΎΠ³ΠΎ Π΅ΠΌΡ ΠΏΠΎΡΠΎΠΊΠ°. ΠΠΎΠ²ΠΈΡΡ exceptionΡ ΡΠ΄ΠΎΠ±Π½ΠΎ ΡΠ°ΠΊ: View / Navigators / Show Breakpoint Navigator / ΠΊΠ½ΠΎΠΏΠΊΠ° Ρ ΠΏΠ»ΡΡΠΎΠΌ ΡΠΏΡΠ°Π²Π° Π²Π½ΠΈΠ·Ρ / Add Exception Breakpointβ¦ / ΠΡΠ°Π²ΠΎΠΉ ΠΊΠ½ΠΎΠΏΠΊΠΎΠΉ ΠΏΠΎ Π½Π΅ΠΌΡ / Move Breakpoint To / User β ΠΈ ΡΡΠ° ΡΠΎΡΠΊΠ° ΠΏΡΠ΅ΡΡΠ²Π°Π½ΠΈΡ Π±ΡΠ΄Π΅Ρ ΠΏΠΎΡΠ²Π»ΡΡΡΡΡ Π²ΠΎ Π²ΡΠ΅Ρ
ΠΏΡΠΎΠ΅ΠΊΡΠ°Ρ
.
Product / Profile / ΠΈΠ½ΡΡΡΡΠΌΠ΅Π½Ρ Core Data β ΠΏΠΎΠ΄ΡΠΎΠ±Π½ΠΎ ΡΡΡ , ΡΠ°ΠΌ Π΅ΡΡΡ ΡΡΠ±ΡΠΈΡΡΡ.
fetchRequest, you still have toperformFetch:call it up to get results (purely from the documentation, did not try it yourself) - Max Mikheyenko