Core Data ManagedObjectContext and Private Queue, role of parent context - managedobjectcontext

My macOS app needs to periodically download user read-only data (like stock prices). To do this I have built a dual-context system:
#interface MyCoreDataStackManager : NSObject
#property (nonatomic, readonly) NSManagedObjectModel* managedObjectModel;
#property (nonatomic, readonly) NSPersistentStoreCoordinator* persistentStoreCoordinator;
#property (nonatomic, readonly) NSManagedObjectContext* managedObjectContext;
#property (nonatomic, readonly) NSURL* applicationSupportDirectory;
#property (nonatomic, readonly) NSURL* storeURL;
During init, the stack is constructed with an NSSQLiteStoreType and NSMainQueueConcurrencyType.
To be able to do background downloading and processing, I also have a method to create a separate context using the same model and store but with it's own NSPersistentStoreCoordinator. The private context uses NSPrivateQueueConcurrencyType.
-(NSManagedObjectContext *)privateContext
{
NSManagedObjectContext* privateContext = nil;
NSError* error = nil;
// Use the same store and model, but a new persistent store coordinator unique to this context.
NSPersistentStoreCoordinator* privateCoordinator = [[[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]] autorelease];
if (privateCoordinator)
{
NSPersistentStore* privateStore = [privateCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self storeURL] options:nil error:&error];
if (privateStore)
{
privateContext = [[[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType] autorelease];
if (privateContext)
{
[privateContext setPersistentStoreCoordinator:privateCoordinator];
[privateContext setUndoManager:nil];
}
}
}
return (privateContext);
}
This blog post does it a similar way but makes the private context be the parent of the main-thread managedObjectContext:
http://martiancraft.com/blog/2015/03/core-data-stack/
[[self managedObjectContext] setParentContext:[self privateContext]];
This blog post also does it in a similar way but makes the main-thread context be the parent of the private context (under "Strategy 2"):
https://code.tutsplus.com/tutorials/core-data-from-scratch-concurrency--cms-22131
[self.privateManagedObjectContext setParentContext:self.mainManagedObjectContext];
The way mine works right now is that neither context is the parent of the other, they just use the same store and it seems to work fine. This methodology is based on an Apple example for downloading earthquake data which used to be in Obj-C but is now only available in Swift.
https://developer.apple.com/library/archive/samplecode/Earthquakes/Introduction/Intro.html
Why are the first two opposite and what are the pros/cons/differences of doing it each way? Why does the Apple example not use a parent at all?
Furthermore, some examples (in similar cases) show both contexts sharing a single NSPersistentStoreCoordinator but mine (as the examples above) have each context owning its own PSC, even though they each point to the same store file. What is the better way?
I have one case where the user can edit the downloaded data. Would it make a difference there as to who (if any) is the parent context?

Related

Core Data not returning correct data sometimes

I have a custom object (Data) inheriting from NSObject, that needs to be persisted into core data. Hence I created a NSManagedObject (Transaction) that contains Data, something like this:
#interface Transaction (CoreDataProperties)
#property (nonatomic) BOOL m_isUploaded;
#property (nullable, nonatomic, retain) id m_transactionData; // this is the Data class, stored under Transformable
#property (nonatomic) int64_t m_submitDateTimeEpochMilliseconds;
#property (nullable, nonatomic, retain) NSString *m_uuid;
#end
I created one context and did everything related to the context on the main thread.
I made Data comply with NSCoding and NSCopying protocols. Also made some custom classes used in Data comply with NSCoding as well.
This is a short extract from Data.h:
#interface Data : NSObject <NSCoding, NSCopying>
#property (strong, nonatomic, nullable) HeldItem *m_heldItem;
#property (strong, nonatomic, nullable) NSDecimalNumber *m_discountAmount;
#property (strong, nonatomic, nonnull) NSMutableArray<Record *> *m_records;
#property (strong, nonatomic, nonnull) NSString *m_transactionId;
#property (nonatomic) CLLocationCoordinate2D m_location;
#property (strong, nonatomic, nullable) NSString *m_status;
- (void)encodeWithCoder:(nonnull NSCoder *)aCoder;
- (nonnull id)initWithCoder:(nonnull NSCoder *)aDecoder;
...lots of variables/methods here...
#end
If I purely just do insert, there are no issues (I confirmed this by looking at the variables during encodeWithCoder:). Or if I do purely just reading, there aren't no issues as well.
However, if I were to insert a new Transaction (and Data) record, and search/read an existing record and modify it (Data), the existing record doesn't get saved, and the new record is mostly blank (as if new). This issue happens at random. I could run it 8 times before I encounter this situation.
Any idea where I might have gone wrong? I've been stuck for quite some time.
This is the part where it seems to give me the problem:
// Store the existing/new data into Core Data
Transaction *t = [MANAGER createTransaction];
Data *data = [Data new];
t.m_kmsTransactionData = data;
// ...some assigning stuffs to other variables...
// ========== if this whole section is omitted the newly created record saves fine =========
Transaction *old = [MANAGER getTransactionFromCoreDataWithId:someOldDataIdString];
// oldData sometimes is returning a blank object (i.e. booleans are no, objects are nil etc)
Data *oldData = [old.m_kmsTransactionData mutableCopy];
oldData.m_status = #"old"; // testing to see if the old record gets updated
old.m_isUploaded = NO;
old.m_kmsTransactionData = [oldData copy];
// This doesn't work because oldData.m_transactionId is nil
data.m_transactionId = [MANAGER generateNewTransactionIDBasedOn:oldData.m_transactionId];
//===============================================
// ... more assigning code here ....
// Save context
[MANAGER saveDatabase];
-
// Some helper function in some manager
- (Transaction * _Nonnull)getTransactionFromCoreDataWithId:(NSString * _Nonnull)dataId
{
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:[Transaction entityName]];
request.returnsObjectsAsFaults = NO;
NSError *error;
NSArray<Transaction *> *fetchedObjects = [self.m_coreDataStore.managedObjectContext executeFetchRequest:request error:&error];
NSAssert(fetchedObjects != nil, #"Failed to execute %#: %#", request, error);
if(fetchedObjects == nil || fetchedObjects.count == 0)
{
// error
return nil;
}
for(Transaction *t in fetchedObjects)
{
Data *td = t.m_kmsTransactionData;
if([td.m_salesRecordId isEqualToString:salesRecordId])
{
return t;
}
}
return nil;
}
I thought it was a Core Data quirk with transformable properties but it wasn't.
The part where I was
// ...some assigning stuffs to other variables...
I assigned m_salesRecordId to an existing id, which was already inside the database. So while searching through the getTransactionFromCoreDataWithId: method, it returned the newly created object based on the m_salesRecordId I've just made earlier.
As for why it worked sometimes and it didn't work sometimes was because the fetched query results wasn't sorted (I assume the order in which the results are returned are always not in sequence), so whichever record was returned first got returned as the searched object (which could either be the empty object I just created or the actual object that was already inside).
I still could not understand yet as to why even though the m_salesRecordId was nil after the screwup, the rest of the variables in the object could not be saved. But since everything is working as intended I'll take the win for now.
tl;dr:
Check your program logic again first

Can't bind NSArray with NSArrayController

I have an array (_websites) which returns 2 results (i can see the records using NSLog).
What I am trying to do is to display those 2 records in NSTableView that has 3 columns. I make numerous attempts to bind the content of my array with the NSArrayController, without any success.
Here is the .h file
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
#interface CombinedViewController : NSViewController <NSTableViewDataSource>
#property (nonatomic,strong) NSManagedObjectContext *mObjContext;
#property AppDelegate *appDelegate;
#property (strong) IBOutlet NSArrayController *combinedRecordsArrayController;
#property (nonatomic,strong)NSArray *websites;
#property (weak) IBOutlet NSTableView *tableView;
#end
the .m file code:
#import "CombinedViewController.h"
#import "Website.h"
#import "Customer.h"
#import "Hosting.h"
#interface CombinedViewController ()
#end
#implementation CombinedViewController
- (void)viewDidLoad {
[super viewDidLoad];
_appDelegate = (AppDelegate*)[[NSApplication sharedApplication] delegate];
self.mObjContext = _appDelegate.managedObjectContext;
[self getCombinedResutls];
}
-(NSArray *)getCombinedResutls {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Website" inManagedObjectContext:self.mObjContext];
[fetchRequest setEntity:entity];
NSError *error = nil;
NSArray *fetchedObjects = [self.mObjContext executeFetchRequest:fetchRequest error:&error];
if (fetchedObjects == nil) {
NSLog(#"Error:%#",error);
}
_websites = [_mObjContext executeFetchRequest:fetchRequest error:nil];
for (Website *ws in _websites) {
Customer *cust = ws.customerInfo;
Hosting *host = ws.hostingInfo;
NSLog(#"Website: %#, Customer: %#, Hosting Provider: %#",ws.websiteUrl, cust.customerName, host.hostingProvider);
}
return fetchedObjects;
}
#end
I am trying to learn how to do it both ways, using cocoa binding and programmatically, so any kind solution will be appreciated. Links with some up to date tutorial will be also very welcomed.
I just forgot to mention...I bind the NSArrayController with ContentArray in Controller Content, and then I bind the NSTableView with the NSArrayController, as well as my Table Column,but I am getting empty NSTableView...no error is shown in console whatsoever.
If you use direct iVar access-- _websites-- to write the websites property's iVar, the KVO notification that the binding depends upon never happens.
If you instead use self.websites = or more explicitly [self setWebsites: ..., then you will trigger a KVO notification to the array controller that the value of the websites property has been updated.
Instead, the array controller in the Xib is unarchived and bound to websites before viewDidLoad, so at that point, websites is nil. And subsequently, you never trigger any KVO notification about websites changing value because you explicitly avoid using the websites accessor setWebsites and instead use direct instance variable access. So the AC never knows that websites changes and the table never reflects any value for websites except nil.
In general never use the instance variable to access a property's value unless you have a very good reason to do so and fully understand why you're doing so.

Deleting Core Data objects from NSMutableArray

I've got an NSMutableArray that currently holds a bunch of Core Data entities like so:
NSFetchRequest *fetchReq = [[NSFetchRequest alloc]init];
[fetchReq setEntity:[NSEntityDescription entityForName:#"Subject"
inManagedObjectContext:self.managedObjectContext]];
subjectsArray = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
I then put this array in a dictionary:
_childrenDictionary = [NSMutableDictionary new];
[_childrenDictionary setObject:subjectsArray forKey:#"SUBJECTS"];
This dictionary is then used as the data for a Source List I have implemented. What I'm having issues with is deleting an object from the subjectsArray. I've tried this with the following code:
_selectedSubject = [subjectsArray objectAtIndex:0];
[subjectsArray removeObject:subjectToDelete];
Now this works fine, when I execute the following:
for (Subject *s in subjectsArray) {
NSLog(#"Subjects: %#", [s title]);
}
The Subject I selected to delete is no longer there and the Source List I have updates correctly after calling:
[_sidebarOutlineView reloadData];
The problem I am having though is that when I quit the application and open it up again, the Subject I previously deleted is still there.
The fetching of the core data entities into the array and dictionary is done inside applicationDidFinishLaunching. At the moment all this code is inside the AppDelegate file, which has a .h file that looks like this:
#import <Cocoa/Cocoa.h>
#import "Subject.h"
#interface LTAppDelegate : NSObject <NSApplicationDelegate, NSOutlineViewDelegate, NSOutlineViewDataSource, NSMenuDelegate> {
IBOutlet NSWindow *newSubjectSheet;
IBOutlet NSWindow *newNoteSheet;
IBOutlet NSWindow *newEditSheet;
NSMutableArray *subjectsArray;
}
#property Subject *selectedSubject;
#property (assign)IBOutlet NSOutlineView *sidebarOutlineView;
#property NSArray *topLevelItems;
#property NSViewController *currentContentViewController;
#property NSMutableDictionary *childrenDictionary;
#property NSArray *allSubjects;
Any ideas as to what is causing to deleted Subject to reappear?
You are removing the objects from the local array, but not the CoreData store itself.
To remove an object from the Core Data store, you need to call managedObjectContext
deleteObject:theObject on it, and then call managedObjectContent save:&error to persist it.
If you are using table views, I would recommend checking out the NSFetchedResultsController as well.
You need to commit your changes to the NSManagedObjectContext for it to persist.
check out the save:&errorOut method on that class for details.

Localizing Core Data model properties for display

I'm working on an iOS project that uses a large and fairly complex data model. Some of the entities in the model have corresponding detail view controllers, which include table views that should display localized names and the corresponding values of certain properties.
I've looked at some of Apple's documentation for creating a strings file for a managed object model, but most of it seems geared toward displaying error messages generated by the SDK rather than accessing localized property names directly.
I created a strings file ("ModelModel.strings") for my model file ("Model.xcdatamodel"), and verified that it is loading correctly by looking at -localizationDictionary on my NSManagedObjectModel instance. My question is: how should I access the localized entity and property names in my code? Is there a way to get to them via NSEntityDescription, NSPropertyDescription, etc. or do I have to go through the NSManagedObjectModel every time?
I'm new at localization, so maybe the answer is obvious, but if so, feel free to just give me a nudge in the right direction.
Update
Following #ughoavgfhw's answer, I quickly came up with two categories to accomplish what I needed. Gist: https://gist.github.com/910824
NSEntityDescription:
#interface NSEntityDescription (LocalizedName)
#property (nonatomic, readonly) NSString *localizedName;
#end
#implementation NSEntityDescription (LocalizedName)
#dynamic localizedName;
- (NSString *)localizedName {
static NSString *const localizedNameKeyFormat = #"Entity/%#";
NSString *localizedNameKey = [NSString stringWithFormat:localizedNameKeyFormat, [self name]];
NSString *localizedName = [[[self managedObjectModel] localizationDictionary] objectForKey:localizedNameKey];
if (localizedName) {
return localizedName;
}
return [self name];
}
#end
NSPropertyDescription:
#interface NSPropertyDescription (LocalizedName)
#property (nonatomic, readonly) NSString *localizedName;
#end
#implementation NSPropertyDescription (LocalizedName)
#dynamic localizedName;
- (NSString *)localizedName {
static NSArray *localizedNameKeyFormats = nil;
if (!localizedNameKeyFormats) {
localizedNameKeyFormats = [[NSArray alloc] initWithObjects:#"Property/%#/Entity/%#", #"Property/%#", nil];
}
for (NSString *localizedNameKeyFormat in localizedNameKeyFormats) {
NSString *localizedNameKey = [NSString stringWithFormat:localizedNameKeyFormat, [self name], [[self entity] name]];
NSString *localizedName = [[[[self entity] managedObjectModel] localizationDictionary] objectForKey:localizedNameKey];
if (localizedName) {
return localizedName;
}
}
return [self name];
}
#end
There is no direct way to get that information provided by apple, but you could implement it yourself. You just need to add categories to NSEntityDescription, etc. which create the identifier and ask for the localized value from the model, and then treat it as if it were built in.
Here is an example NSEntityDescription implementation. For properties, you would do something similar, but you should use both the entity and property name in case multiple entities have properties with the same name (you may also need to use both the entity and property name as keys in your localization file. I don't know if the model will create them automatically).
#implementation NSEntityDescription (Localization)
- (NSString *)localizedName {
NSString *key = [NSString stringWithFormat:#"Entity/%#", [self name]];
NSDictionary *dictionary = [[self managedObjectModel] localizationDictionary];
NSString *localizedName = [dictionary objectForKey:key];
return (localizedName ? localizedName : [self name]);
}
#end
Here is a reference for the keys used in the localizations.

Pass array from one Objective-C class to another

Im attempting to pass an array that is created in one class into another class. I can access the data but when I run count on it, it just tells me that I have 0 items inside the array.
This is where peopleArray's data is set up, it's in a different class than the code that is provided below.
[self setPeopleArray: mutableFetchResults];
for (NSString *existingItems in peopleArray) {
NSLog(#"Name : %#", [existingItems valueForKey:#"Name"]);
}
[peopleArray retain];
This is how I get the array from another class, but it always prints count = 0
int count = [[dataClass peopleArray] count];
NSLog(#"Number of items : %d", count);
The rest of my code:
data.h
#import <UIKit/UIKit.h>
#import "People.h"
#class rootViewController;
#interface data : UIView <UITextFieldDelegate>{
rootViewController *viewController;
UITextField *firstName;
UITextField *lastName;
UITextField *phone;
UIButton *saveButton;
NSMutableDictionary *savedData;
//Used for Core Data.
NSManagedObjectContext *managedObjectContext;
NSMutableArray *peopleArray;
}
#property (nonatomic, assign) rootViewController *viewController;
#property (nonatomic, retain) NSManagedObjectContext *managedObjectContext;
#property (nonatomic, retain) NSMutableArray *peopleArray;
- (id)initWithFrame:(CGRect)frame viewController:(rootViewController *)aController;
- (void)setUpTextFields;
- (void)saveAndReturn:(id)sender;
- (void)fetchRecords;
#end
data.m(some of it at least)
#implementation data
#synthesize viewController, managedObjectContext, peopleArray;
- (void)fetchRecords {
[self setupContext];
// Define our table/entity to use
NSEntityDescription *entity = [NSEntityDescription entityForName:#"People" inManagedObjectContext:managedObjectContext];
// Setup the fetch request
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
// Define how we will sort the records
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"Name" ascending:NO];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
[request setSortDescriptors:sortDescriptors];
[sortDescriptor release];
// Fetch the records and handle an error
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (!mutableFetchResults) {
// Handle the error.
// This is a serious error and should advise the user to restart the application
}
// Save our fetched data to an array
[self setPeopleArray: mutableFetchResults];
for (NSString *existingItems in peopleArray) {
NSLog(#"Name : %#", [existingItems valueForKey:#"Name"]);
}
[peopleArray retain];
[mutableFetchResults release];
[request release];
//NSLog(#"this is an array: %#", eventArray);
}
login.h
#import <UIKit/UIKit.h>
#import "data.h"
#class rootViewController, data;
#interface login : UIView <UITextFieldDelegate>{
rootViewController *viewController;
UIButton *loginButton;
UIButton *newUser;
UITextField *entry;
data *dataClass;
}
#property (nonatomic, assign) rootViewController *viewController;
#property (nonatomic, assign) data *dataClass;
- (id)initWithFrame:(CGRect)frame viewController:(rootViewController *)aController;
- (BOOL)textFieldShouldReturn:(UITextField *)theTextField;
#end
login.m
#import "login.h"
#import "data.h"
#interface login (PrivateMethods)
- (void)setUpFromTheStart;
- (void)loadDataScreen;
-(void)login;
#end
#implementation login
#synthesize viewController, dataClass;
-(void)login{
int count = [[dataClass peopleArray] count];
NSLog(#"Number of items : %d", count);
}
Is it the same object? If so, what you have should work. Check to see how you are getting the dataClass instance -- if you alloc a new one, you don't get the array from the other object.
Edit: From your comments below, it appears that you are having some confusion on the difference between classes and objects. I will try to explain (I'm going to simplify it):
A class is what you write in Xcode. It's the description that lets your application know how to create and access objects at run-time. It is used to figure out how much memory to allocate (based on instance variables) and what messages can be sent, and what code to call when they are. Classes are the blueprints for creating objects at runtime.
An object only exists at run-time. For a single class, many objects of that class can be created. Each is assigned its own memory and they are distinct from each other. If you set a property in one object, other objects don't change. When you send a message to an object, only the one you send it to receives it -- not all objects of the same class.
There are exceptions to this -- for example if you create class properties (with a + instead of a - at the beginning), then they are shared between all objects -- there is only one created in memory, and they all refer to the same one.
Also, since everything declared with a * is a pointer -- you could arrange for all pointer properties to point to the same data. The pointer itself is not shared.
Edit (based on more code): dataClass is nil, [dataClass peopleArray] is therefore nil, and then so is the count message call. You can send messages to nil, and not crash, but you don't get anything useful.
I don't see how the login object is created. When it is, you need to set its dataClass property.
Try running the code in the debugger, setting breakpoints, and looking at variables.
From the code, it looks like you are passing a mutable array.
[self setPeopleArray: mutableFetchResults];
Probably the items of the array are removed somewhere in your calling class / method. Or the array is reset by the class from which you get the mutableFetchResults in the first place.