I'd like to undo a save-operation on my NSManagedObjectContext; I always thought, that's what the NSUndoManager is for, but it seems as if undoing over a save-operation doesn't work...
An example:
[[NSManagedObjectContext MR_contextForCurrentThread] setUndoManager:[NSUndoManager new]];
[[NSManagedObjectContext MR_contextForCurrentThread].undoManager beginUndoGrouping];
MyDataObject *mdo = [MyDataObject MR_createInContext:[NSManagedObjectContext MR_contextForCurrentThread]];
mdo.name = #"...";
[[NSManagedObjectContext MR_contextForCurrentThread] save:nil];
[[NSManagedObjectContext MR_contextForCurrentThread].undoManager endUndoGrouping];
[[NSManagedObjectContext MR_contextForCurrentThread].undoManager undo];
But the insertion is not undone... is there no way to achieve this? Like a transaction?
Could you use a child managed object context to do your save (which pushes it up to the parent, but doesn't touch the file on disk), and then do a rollback on the parent if you want to undo it?
I think calling save is like committing all the changes you've made since the last save, and rollback is like discarding them. Once you've committed the changes, they're in the persistent store and there's nothing keeping transaction logs in order to support rolling back.
Can you explain a bit more about why you want a save in the middle of your transaction?
Related
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.
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.
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];
}
I have a NSFetchedResultController with this predicate set:
NSPredicate* pred = [NSPredicate predicateWithFormat:#"author != %# && deleted != %#", [NSNumber numberWithLongLong:0],[NSNumber numberWithBool:YES]];
It filters fine on startup, and I get a delegate callback and list updates fine if objects are added or deleted.
But if I change the "deleted" field, the NSFetchedResultController set is NOT updated, nor do I get the callback.
Though, the actual object in the NSFetchedResultController is updated, if I do a "reloadData" on my table and check the value of "deleted", it is actually set to YES.
Why is it not disappearing from the NSFetchedResultController?
Is this expected behavior?
Or what could I be doing wrong?
I guess I solved it...
Turns out I actually do get the callback ("didChangeObject") and I now check the value of "deleted" here and manually remove it from the NSFetchedResultController if the value is YES.
Seems to work, but I still did expect it to be automatic.
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.