Persist data between views - core-data

I am developing an app in which the user enter a search word and receives results from a web service (I control the web service). The results are displayed on a uitable view and then the user can select a row and navigate to three - four level of inner details (I am using the uinavigation control).
I am wondering what is the best way to persist the data between the views.
I am currently using the application delegate to store an array of objects which I can access from everywhere in the app. It works fine but I read that it is not a good practice for the job (I am concerned about memory issues) . I tried using Core Data framework for the job but than I realized that I would have to read my web service results, and loop them one by one in order to insert them to database. I also will have to delete old data because the data I am saving is only actual for the current search. I read about p-lists and saving data to file system but could not find a real good solution for my job...
Any help will be appreciated!!! Thanks!!!

You can use JSON or PLISt for communication, although binary plist is - according to apple - much faster on the phone side.
Creating a dictionary from the binary Plist is relatively simple:
NSPropertyListFormat format;
NSDictionary *myDictionary = [NSPropertyListSerialization
propertyListFromData:data mutabilityOption:NSPropertyListMutableContainers
format:&format errorDescription:&errorString];
Creating it from a JSON just requires using one of the readily available JSON libraries. Once you have the dictionary, save it:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,
NSUserDomainMask, YES);
NSString *file = [NSString stringWithFormat: #"%#%#.plist", [paths objectAtIndex:0], #"MyPlistBaseName"];
[plist writeToFile: file atomically:NO];
And re-load it later:
NSDictionary *plist = [[[NSDictionary alloc] initWithContentsOfFile: file] autorelease];
However, If you encapsulate the access to this data in a singleton, then you can worry about optimizing the implementation if speed / memory becomes an issue. Just treat that signleton as the 'owner' of the data, with methods like count, getGroup(0) (to return a block of 25), etc. Then you can hide all the implementation details inside the object.

Related

Making multiple NSUserActivity instances searchable

I'm planning to make some of my app content publicly indexable, and for that I am using NSUserActivity. From my experiments so far, I've discovered that apparently the only activity that appears in the search results is the last one to get becomeCurrent called on. Is there a way to make all my activities searchable?
The following code is on my appDelegate:
for (Shop* shop in shopManager)
{
NSUserActivity* activity = [[NSUserActivity alloc] initWithActivityType:ACTIVITY_OPEN_SHOP];
activity.userInfo = #{#"additional1": shop.name};
activity.eligibleForPublicIndexing = YES;
activity.eligibleForSearch = YES;
activity.keywords = shop.indexableKeywords;
CSSearchableItemAttributeSet* attributeSet = [[CSSearchableItemAttributeSet alloc] initWithItemContentType:(NSString*)kUTTypeText];
attributeSet.title = shop.name;
attributeSet.contentDescription = shop.indexableDescription;
attributeSet.keywords = [shop.indexableKeywords allObjects];
[activity setContentAttributeSet:attributeSet];
[activity becomeCurrent];
[activities addObject:activity];
}
self.userActivities = [[NSSet alloc] initWithArray:activities];
Hey I have code that is very similar to yours and spotlight is able to index all of my NSUserActivity objects. My guess is that your NSUserActivity objects go out of reference as soon as the next iteration of the loop occurs. Try adding a strong property.
From this source on Apple Forums:
https://forums.developer.apple.com/message/13640#13640
In my case, I had code that was allocating the NSUA, setting some
properties on it, calling becomeCurrent, but then the object would go
out of scope and deallocated. If you're doing this, try tossing the
activity into a strong property to see if you can then see the results
when you search.
Let me know if it still doesn't work.
RequiredUserInfoKeys is the property of NSUserActivity that you have to set in order to work properly in search results.
activity.requiredUserInfoKeys = [NSSet setWithArray:#[#"additional1"]];
I have met the same problem. I think the reason of this is that previous user activity has not enough time for indexing its metadata by system, the next user activity have became current user activity, so only last one is searchable.
My solution is put latter one into a dispatch_after block and delay 1.5 second, making each of them has time to be indexed.
If someone has a better solution, I would be grateful.

Is it possible to have multiple core data "databases" on one iOS app?

I'm wanting to write a "management" game that utilizes Core data heavily. The game requires a pre-set, pre-defined dataset that cannot be changed by the user/system; it is used to seed the game with data and is meant to be read-only.
The best example I can give is a football management game, but it could be anything. In some football management sims they give you scenarios and pre-set datasets.
As the user proceeds through the game they can save/load their progress which is saved to the core data.
In addition to this, the user can receive updates to the pre-defined data or can purchase scenarios packs of data; which is saved to their device.
So, there could be multiple "core data databases" (yes, I know core data isn't strictly a database) or "buckets" which the app can dive into and use.
The schema of the data would not change.
So we have:
Pre-defined data (Default data) that is only used for seeding the game.
The user's current save game.
The user has downloaded a scenario from the Internet.
Problem: What happens when the user saves the game whilst on a "scenario".
Problem: How do I keep track of all the scenarios and all the user saved games in core data?
This sounds like multiple databases at a given time. Obviousily one should restrict how many save games a user can make.
An alternative solution to this is that the user's device exports a back-up copy of the data in JSON or XML and this serves as the "save data" and I could use this strategy for scenarios too. Obviousily some kind of encryption would be needed to prevent people simply changing stats in the game via the XML.
But I'm wondering from the outset what would be the best way to use Core data for iOS devices handle more than 1 core data "database"?
Thanks for your time
If the data models are the same, you can just setup your MOC so that it uses both persistent stores... one which is read-only and the other that is read/write.
Or, you could use separate MOC for each store.
So, how you want to use it is your only decision factor, since you can have almost any combination of MOC/PSC.
Look at the documentation here for more information.
Edit:
The link given with this question is dead, someone else suggested this link in another deleted answer.
NB: This is an old question, but the problems it describes are timeless, so I've written the answer as if the question were posted today.
Actually, none of this suggests the need for multiple databases. So we have:
1) Pre-defined data (Default data) that is only used for seeding the
game.
Write a method that loads the data into the persistent store (database). Set a flag in user default, defaultDataHasBeenLoaded or something like that, and check that in the appDelegata.
2) The user's current save game.
You need a Users table and a Games table with a one-to-many relationship. In the Games table you add an isCurrentGame attribute.
3) The user has downloaded a scenario from the Internet.
Now it's getting interesting. You will need an import function or class for that and you'll want to run that on a background thread. That way, your user can continue playing, or looking looking at their scores or whatever, while the new scenario is being imported. When the scenario has been imported, the user should get a notification and the opportunity to switch to the new scenario.
The most efficiënt way to do this is to use NSPeristentContainer which is available from iOS 10.0, macOS 10.12, tvOS 10.0 and watchOS 3.0. Give NSPeristentContainer the name of the data model and it will create or load a persistent store and set the persistentStoreCoördinator and the managedObjectContext.
// AppDelegate.h or class header file
#property (readonly, strong, nonatomic) NSPersistentContainer *persistentContainer;
#property (readonly, weak, nonatomic) NSManagedObjectContext *managedObjectContext;
// AppDelegate.m or other implementation file
#synthesize persistentContainer = _ persistentContainer;
#synthesize managedObjectContext = _ managedObjectContext;
- (NSPersistentContainer *)persistentContainer
{
#synchronized (self) {
if (_persistentContainer == nil) {
_persistentContainer = [[NSPersistentContainer alloc] initWithName:#"nameOfDataModel"];
[_persistentContainer loadPersistentStoresWithCompletionHandler:^(NSPersistentStoreDescription *storeDescription, NSError *error) {
if (error != nil) {
// Handle the error
} else {
_managedObjectContext = _persistentContainer.viewContext; // NB new name for moc is viewContext!
}
}];
}
}
return _persistentContainer;
}
To use the the container from the appDelegate in an NSViewController, you add the following to viewDidLoad:
self.representedObject = [(AppDelegate *)[[NSApplication sharedApplication] delegate] persistentContainer];
// Use representedObject in bindings, such as:
[_gameNameTextField bind:NSValueBinding toObject:self
withKeyPath:#"representedObject.game.name"
options:options];
To import the new scenario, use performBackgroundTask:, a block which will automatically create a new thread and a new managedObjectContext (here called moc_background). Use only moc_background for anything you do in the block -- if you call a method outside the block, pass it moc_background.
NSPersistentContainer *pc = (NSPersistentContainer *)self.representedObject;
pc.viewContext.automaticallyMergesChangesFromParent = YES; // this will ensure the main context will updated automatically
__block id newScenario;
[pc performBackgroundTask:^(NSManagedObjectContext * _Nonnull moc_background) {
NSEntityDescription *scenarioDesc = [NSEntityDescription entityForName:#"Scenario" inManagedObjectContext:moc_background];
NSManagedObject *scenario = [[NSManagedObject alloc] initWithEntity:scenarioDesc insertIntoManagedObjectContext:moc_background];
// configure scenario with the data from newScenario
NSError *error;
BOOL saved = [moc_background save:&error];
// send out a notification to let the rest of the app know whether the import was successfull
}];
Problem: What happens when the user saves the game whilst on a
"scenario".
That depends on who gets there first, the background thread that attempts to merge or the save operation. If you add a Scenario table with many-to-one relationship to the Game table, there should not be any problems.
Problem: How do I keep track of all the scenarios and all the user
saved games in core data?
Data modeling can be tricky. Keep it simple at first and add tables and relationships when you find a clear need for them. And then test, test, test.

RestKit - delete an object based on incoming attribute

I am using basic RestKit mapping with JSON I am receiving via a REST call.
RKManagedObjectMapping* contactMapping = [RKManagedObjectMapping mappingForClass:[Contact class]];
[contactMapping mapAttributes:#"id", #"firstName", #"middleName", #"lastName", #"title", #"email", nil];
contactMapping.primaryKeyAttribute = #"id";
[contactMapping mapRelationship:#"addresses" withMapping:addressMapping];
[contactMapping mapRelationship:#"phoneNumbers" withMapping:phoneMapping];
// Register our mappings with the provider
[objectManager.mappingProvider setMapping:contactMapping forKeyPath:#"contacts"];
I have a new field, status, which I now need to check to see if it's "Delete" (or whatever) and if it is, delete that contact from the database, if they exist. However I'm at a loss as to where to put this. Do I need to create my own extended Object Mapping class to handle this? Are there any examples anywhere of this? My Google Fu is weak on this one.

How do you correctly update a model in Xcode4 without corrupting it?

I never had any problems with Xcode3, but with Xcode4 I'm getting Apple's code failing approx 1 time in 3 when I update a core data model, with the dreaded "Persistent store migration failed, missing source managed object model." error.
Here's my setup (how I configured the project to auto-migrate):
NSPersistentDocument, from Apple's template
Override Apple's model-loading method, and the ONLY thing I do is to provide the two flags in the storeOptions Dictionary, which turn on auto-migration
-(BOOL)configurePersistentStoreCoordinatorForURL:(NSURL *)url ofType:(NSString *)fileType modelConfiguration:(NSString *)configuration storeOptions:(NSDictionary *)storeOptions error:(NSError **)error
{
NSMutableDictionary *newOptions = nil;
if( storeOptions != nil )
newOptions = [NSMutableDictionary dictionaryWithDictionary:storeOptions];
else
newOptions = [NSMutableDictionary dictionary];
[newOptions setValue:#"YES" forKey:NSMigratePersistentStoresAutomaticallyOption];
[newOptions setValue:#"TRUE" forKey:NSInferMappingModelAutomaticallyOption];
BOOL success = FALSE;
success = [super configurePersistentStoreCoordinatorForURL:url ofType:fileType modelConfiguration:configuration storeOptions:newOptions error:error];
return success;
}
Here's the process I've been using (which is already working around 1 bug in Xcode4!)
Select the model (named "something.xcdatamodel" in Xcode4, with a twisty on the left)
Go to Editor menu, select "Add new model version..."
Name the new version 1 integer higher than last - e.g. if previous was "4" name the new one "5"
In the right-hand pane, change the current model version to the newly-created one
workaround for XCode4 bug: select any file, then select the newly-created model. If you do not, Xcode shows the selection on the newly-created model, but will edit the previous model instead, which definitely corrupts everything in CoreData
Edit your model; in this case, I'm adding a new attribute to an existing entity
Save. Build. Run. ... CRASH.
Except, as I said, approx 2 times in 3 this works correctly. Once it works once, it's (obviously) fine - the lightweight migration is complete, the next save saves in the new model version.
So I'm guessing there's something I'm doing wrong in the above steps, but I've been through the docs 5 or 6 times and can't see anything obvious. Doesn't help that NSPersistentDocument docs are all out of date - but I've done lightweight migration on iPhone lots of times too, so I'm reasonably confident with doing this, and it seems right to me.
Other things I've tried/checked:
- iPhone Core Data Lightweight Migration Cocoa error 134130: Can't find model for source store (nope; only the root xcdatamodel was being included)
Use [NSNumber numberWithBool:YES] not #"YES" or #"TRUE".
Since you have eliminated a corrupt development store as a source of the problem, I suspect the problem lays in Xcode 4.x which is buggy to say the least. A lot of people are reporting similar issues but no two problems seem exactly the same. It is probably a bug/s that only occur with specific data model setups so the problem will be very hard to track down.
You may simply have to abandon automatic migration and create an explicit migration map. It takes longer and introduces complexity into your code but it will always work.
If you have a shipping app and will be dealing with end user data in the wild, you do have a moral and business obligation to take the extra step to protect end user data.
I was getting super-confused but this, and it WASN'T working.. because I was assuming that the method would already HAVE a "store options" dictionary.. I just needed to check for it's existence before i set the aforementioned options…
-(BOOL)configurePersistentStoreCoordinatorForURL: (NSURL*)u
ofType: (NSString*)t
modelConfiguration: (NSString*)c
storeOptions:(NSDictionary*)o
error: (NSError**)e
{
return [super configurePersistentStoreCoordinatorForURL:u
ofType:t
modelConfiguration:c
storeOptions:
o ? [o dictionaryWithValuesForKeys:
#[ NSMigratePersistentStoresAutomaticallyOption, #YES,
NSInferMappingModelAutomaticallyOption, #YES]]
: #{ NSMigratePersistentStoresAutomaticallyOption :#YES,
NSInferMappingModelAutomaticallyOption :#YES}
error:e];
}

Core Data and NSOperation

I'm currently working with an NSPersistentDocument subclass that uses NSOperation to import data in the background. As per the documentation, I'm observing the NSManagedObjectContextDidSaveNotification after saving in the background task and propagating the notification to the NSManagedObjectContext in the main thread using -mergeChangesFromContextDidSaveNotification:.
Everything works fine, but it presents a weird workflow for a user who's importing data into a new document. They need to save an empty document before doing the import (otherwise the -save: fails because the document hasn't configured a URL for the NSPersistentStoreCoordinator.) I don't see a way around this other than some kind of "new document setup" wizard that ensures -writeToURL:ofType:forSaveOperation:originalContentsURL:error: gets called before the import.
Also, it appears that an import task in the background precludes the use of an NSUndoManager on the main thread. (I'm assuming that it's unsafe to share the managed object context's undo manager across the threads.) From a user's point-of-view, there's no way to undo all the new objects created during the import.
I've read both the Core Data Programming Guide and Marcus Zarra's book, but I'm still new to this aspect of the framework. Hopefully, I've overlooked something: if not, I'll adapt my app to these restrictions (the benefits of Core Data far outweigh these user interface limitations.)
Thanks for your time!
--
Based on Peter Hosey's suggestion below, I added the following code to create a temporary persistent store prior to the import:
NSPersistentStoreCoordinator *persistentStoreCoordinator = [self.managedObjectContext persistentStoreCoordinator];
if ([[persistentStoreCoordinator persistentStores] count] == 0) {
// create an in-memory store to use temporarily
NSError *error;
NSPersistentStore *persistentStore = [persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error];
if (! persistentStore) {
NSLog(#"error = %#", error); // TODO: better error handling
}
}
Then, after a file is selected in the save panel, the temporary persistent store is migrated to a SQLite store at the selected URL:
- (BOOL)writeToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation originalContentsURL:(NSURL *)absoluteOriginalContentsURL error:(NSError **)error
{
NSPersistentStoreCoordinator *persistentStoreCoordinator = [self.managedObjectContext persistentStoreCoordinator];
for (NSPersistentStore *persistentStore in [persistentStoreCoordinator persistentStores]) {
if (persistentStore.type == NSInMemoryStoreType) {
// migrate the in-memory store to a SQLite store
NSError *error;
NSPersistentStore *newPersistentStore = [persistentStoreCoordinator migratePersistentStore:persistentStore toURL:absoluteURL options:nil withType:NSSQLiteStoreType error:&error];
if (! newPersistentStore) {
NSLog(#"error = %#", error); // TODO: better error handling
}
}
}
return [super writeToURL:absoluteURL ofType:typeName forSaveOperation:saveOperation originalContentsURL:absoluteOriginalContentsURL error:error];
}
I'm nobody's Core Data expert, but from what I can tell from the docs, you'll want to start with an in-memory store until the user (in their own time) saves the document. Then, send the coordinator a migratePersistentStore:toURL:options:withType:error: message to change over from the in-memory store to the new truly-persistent store. See that document for some essential details (particularly regarding the fate of the store you migrate).
My first thought on the workflow/saving part would be, if a persistent store hasn't yet been created for the document, to create a temporary in-memory store, so that the imported data would be saved to that store instead (though the document/window would still be marked as dirty). Then, once the user saves the document for real, you would reconfigure the coordinator to remove the in-memory store and replace it with the on-disk store, so all further saves would go to disk.
I'm not 100% familiar with the Mac stuff, but I'm sure you could you use an in-memory persistent store before the user has saved, and then add the sql/plist store after that action.
Potentially even better could be to create a on-disk persistent store in a standard temporary directory, and move it across when the user clicks to save.
Have you tried setting up a temporary file URL when setting up the coordinator?
You should be able to undo the -mergeChangesFromContextDidSaveNotification: on the main thread. No need to register an undo manager for the MOC on the background thread.

Resources