The app I'm working on lets users manage some assets. The user can create / delete / edit / split / move assets around on the screen. Users need to be able to undo all these steps back.
The assets are managed with core data (and yes, the undoManager is instantiated).
For each of these actions I create undo groupings with this pair:
beginUndoGrouping ... endUndoGrouping
Here's a simple example (sequence 1):
// SPLIT
- (void) menuSplitPiece: (id) sender
{
[self.managedObjectContext.undoManager beginUndoGrouping];
[self.managedObjectContext.undoManager setActionName:#"Split"];
//... do the split
[self.managedObjectContext.undoManager endUndoGrouping];
// if the user cancels the split action, call [self.managedObjectContext.undoManager undo] here;
}
I do the same for edit: if the user cancels the edit, then I call undo right after endUndoGrouping.
Everything works beautiful with one exception: besides the groups that I create, there are other groups being created by Core Data which I cannot control. Here's what I mean by that:
I registered to receive NSUndoManagerDidCloseUndoGroupNotification notifications like so:
- (void) registerUndoListener
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(didCloseUndoGroup:)
name:NSUndoManagerDidCloseUndoGroupNotification object:nil];
...}
I use these notifications to refresh the Undo button and display the name of the action that is being undone as a result of some action: e.g. Undo Split
Yet, didCloseUndoGroup is called / notified twice for every action above (e.g. section 1, after endUndoGrouping):
At the time of the first notification, self.managedObjectContext.undoManager.undoActionName contains the undo action name I've set, which I expected, while the second time undoActionName is an empty string.
As a workaround, I tried to simply undo operations that had an empty name (Assuming they were not mine and I did not need them), and see whether I was missing anything.
Now, didCloseUndoGroup looks like this
- (void) didCloseUndoGroup: (NSNotification *) notification
{
...
if ([self.managedObjectContext.undoManager.undoActionName isEqualToString:#""]){
[self.managedObjectContext.undoManager undo];
}
[self refreshUndoButton]; // this method displays the name of the undo action on the button
...
}
And magically it works, I can undo any command, any number of layers using "undo". But this is not the way it should work...
Several other things I tried before that:
[self.managedObjectContext processPendingChanges] before opening any grouping. It was still sending two notifications.
Another thing I tried was disableUndoRegistration / enableUndoRegistration. This one generated an exception: "invalid state, undo was called with too many nested undo groups"
None of the above helped me "isolate" the mysterious groupings I mentioned before.
I should not be receiving NSUndoManagerDidCloseUndoGroupNotification notifications twice. Or, should I? Is there a better way to deal with this situation?
UPDATE
This is what finally worked. Previously I was automatically undoing the no-name groups as soon as I received a notification. This is what caused the problem. Now, I undo everything until I reach my target group and then I do a last undo for that group.
"undoManagerHelper" is just a stack management system that generates a unique ID for each command that is pushed on the stack. I use this unique ID to name the group.
- (BOOL) undoLastAction
{
NSString *lastActionID = [self.undoManagerHelper pop]; // the command I'm looking for
if (lastActionID == nil) return false;
//... undo until there is nothing to undo or self.managedObjectContext.undoManager.undoActionName equals lastActionID
//the actual undo here
if ([currentActionID isEqualToString: lastActionID] && [self.managedObjectContext.undoManager canUndo]){
[self.managedObjectContext.undoManager undo];
}
return true;
}
- (void) beginUndoGroupingWithName: (NSString *) name
{
[self.managedObjectContext processPendingChanges];
[self.managedObjectContext.undoManager beginUndoGrouping];
NSString *actionID = [self.undoManagerHelper push: name];
[self.managedObjectContext.undoManager setActionName:actionID];
}
- (void) closeLastUndoGrouping
{
[self.managedObjectContext.undoManager endUndoGrouping];
[self.managedObjectContext processPendingChanges];
}
According to the documentation for beginUndoGrouping - https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/Foundation/Classes/NSUndoManager_Class/Reference/Reference.html - "By default undo groups are begun automatically at the start of the event loop, but you can begin your own undo groups with this method, and nest them within other groups." The unnamed group is the default undo group that contains all operations, it sounds like you should ignore the unnamed group for your situation.
Related
'Cause of the difficulties I found combining UITableView and SpriteKit Framework I decided to develop a custom SKTableView (provided with full scrolling system). Everything works really fine but I have serious problem understanding an error I get during the update process.
My SKTableView shows the user a list of items in alphabetical order. Due to the tricky algorithm I should have create in order to update this list (add a cell in a defined position, refresh the headers and scroll down all of the items), I decided to simply update the database and reload the list (by removing existing content and recreate it) while showing asynchrony a notification to the user.
What is happen is the application to success reload the list once, once again, then after some random updates ... here comes the chaos
Issues
The application crashes giving me EXC_BAD_ACCESS during the removeAllChildren methods.
The application crashes with the same error during a for statement (???) or during other statements
The application crashes giving me jet_context::set_fragment_texture... error.
Debug:
The console window doesn't show any kind of information.
I can't monitoring the application using 'Allocation' or 'Zombies' instruments because it critically slow down the application. It takes more than 2 minutes to even load the SKTableView during the init method.
Code
Interface
/** Container for all the table items. */
#property (nonatomic, readonly) SKNode *nodeContainer;
Initialization
- (instancetype)init
{
if (self = [super init]) {
...
Create the style of the SKNode
...
// Container initialization
_nodeContainer = [[SKNode alloc] init];
_nodeContainer.position = CGPointZero;
_nodeContainer.zPosition = 0;
[self addChild:_nodeContainer];
// Generate the content
[self generateContent];
}
return self;
}
Populate the table
- (void)generateContent
{
// Checks if the node exists, if it does, remove all of its children
// Note: used for the recreation of the list
if (_nodeContainer != nil)
[_nodeContainer removeAllChildren];
...
Initializes several SKSpriteNode and SKLabelNode
...
[_nodeContainer addChild:imgItem];
[_nodeContainer addChild:labelItem];
}
Reload the content
...
Update the database
...
[self showProgressNotification];
// Reload the content showing a notification
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
// Reload the content
[self generateContent];
dispatch_async(dispatch_get_main_queue(), ^{
[self hideProgressNotification];
});
});
I really cannot figure it out what's happening. The code crashes overtime in a different line without a logic. I mean, is this the right way to remove and recreate items? Any advise?
I try to subscribe to a signal with throttle, but it never executes.
I have a UISearchController (Attention: UISearchController from iOS8, not the older UISearchDisplayController, which works quiet better and has thousands of working tutorials and examples in the web) and want to make API-Requests while the user is typing.
To let the traffic being low, i don't want to start API-Requests with each key the user is pressing, but when the user stops for a while, say 500 ms after the last keypress.
Since we're unable to reference the TextField in the SearchBar of the UISearchController, we use the delegates from UISearchController:
To get the latest typed text of the Textfield in the Searchbar, I use this:
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
NSString *searchText = searchController.searchBar.text;
// strip out all the leading and trailing spaces
NSString *strippedString = [searchText stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
if([strippedString isEqualToString:self.currentFilter]) {
return;
}
self.currentFilter = strippedString;
}
The property currentFilter keeps the current search string.
Also, i have a RACObserve on the currentFilter-Property to react on every change which is made to this property:
[RACObserve(self, currentFilter) subscribeNext:^(NSString* x) {
NSLog(#"Current Filter: %#", x);
// do api calls and everything else
}];
Now i want to throttle this signal. But when i implement the call to throttle, nothing happens. SubscribeNext will never be called:
[[RACObserve(self, currentFilter) throttle:500] subscribeNext:^(NSString* x) {
NSLog(#"%#", x); // will never be called
}];
How to achieve to throttle inputs in a searchbar? And what is wrong here?
UPDATE
i found a workaround besides ReactiveCocoa thanks to #malcomhall. I moved the code within the updateSearchResultsForSearchController-delegate method into a separate method and schedule it with performSelector and cancel this scheduler with cancelPreviousPerformRequestsWithTarget.
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:#selector(useTextSearchQuery) object:nil];
[self performSelector:#selector(useTextSearchQuery) withObject:nil afterDelay:1.0];
}
Anyway, i want still understand how "throttle" from ReactiveCocoa is working and why not in this case :)
-throttle: accepts an NSTimeInterval, which is a floating-point specification of seconds, not milliseconds.
Given the code in the question, I expect you would see results after 500 seconds have elapsed.
We're having this issue where different threads see different data on the same records but with different managed object contexts (moc). Our app syncs in the background to a server API. All of the syncing is done on it's own thread and using it's own moc. However, we've discovered that when data gets updated on the main moc that change in data is not shown in the background moc. Any ideas what could be happening? Here's some more details: we're using grand central dispatch like so to put the sync operations on it's own thread: We've checked which queue things are running on and it all is happening on the queue expected.
- (void) executeSync; {
dispatch_async(backgroundQueue, ^(void) {
if([self isDebug])
NSLog(#"ICSyncController: executeSync queue:%# \n\n\n\n\n", [self queue]);
for(id <ICSyncControllerDelegate> delegate in delegates){
[delegate syncController:self];
}
if([ICAccountController sharedInstance].isLoggedIn == YES && shouldBeSyncing == YES) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 300ull * NSEC_PER_SEC), dispatch_get_current_queue(), ^{
[self executeSync];
});
}
});
}
here's how we create the background moc and we've confirmed that it's created on the background queue.
- (NSManagedObjectContext*)backgroundObjectContext {
if (_backgroundObjectContext)
return _backgroundObjectContext;
_backgroundObjectContext = [[NSManagedObjectContext alloc] init];
[_backgroundObjectContext setPersistentStoreCoordinator:self.persistentStoreCoordinator];
[_backgroundObjectContext setStalenessInterval:0.0];
return _backgroundObjectContext;
}
I should add that our background moc is requerying for data and those records returned from that action still have the old values for some fields. How does the background moc get the current data that was already saved by the main moc? I thought just by requerying I would get the current state of these records..
by requerying I mean the following:
The background MOC is executing another "query" to get "fresh" data after the records have been changed by the main moc, yet the data has old values - not the updated values seen in the main moc.
+ (NSArray *)dirtyObjectsInContext:(NSManagedObjectContext *)moc {
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"SUBQUERY(memberships, $m, $m.category.name == %# AND $m.syncStatus > %d).#count > 0", MANAGED_CATEGORY_FAVORITES, ManagedObjectSynced];
return [self managedObjectsWithPredicate:predicate inContext:moc];
}
Your help is hugely appreciated as we've been trying to figure this out, or find a work around that doesn't include ditching our threads for days now.
That's how it's supposed to work -- indeed, an important role of the managed object context is to protect you from changes to the data made in other threads. Imagine the havoc that would result if you had a background thread modifying the same objects that the main thread was using without some sort of synchronization scheme.
Read Communicating Changes Between Contexts to learn how to merge changes from one context into another.
I use the following code to listen for changes on context 2, so that context 1 keeps up to date:
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:context1
selector:#selector(contextTwoUpdated:)
name:NSManagedObjectContextDidSaveNotification
object:context2];
it causes this method to be called on context 1, and i invoke the merge method:
- (void)contextTwoUpdated:(NSNotification *)notification {
[context1 mergeChangesFromContextDidSaveNotification:notification];
}
a side effect of this is any NSFetchedResultsController that is attached to context1 will send a variety of messages to its delegate informing it of the changes,
i've never tried listening both ways while the user changes the object and you update them on the user from behind - i suspect you may have to manage merges if that's the case, since it's one-way (and all user driven) for me i assume all updates to be valid
My app is using an NSFetchedResultsController tied to a Core Data store and it has worked well so far, but I am now trying to make the update code asynchronous and I am having issues. I have created an NSOperation sub-class to do my updates in and am successfully adding this new object to an NSOperationQueue. The updates code is executing as I expect it to and I have verified this through debug logs and by examining the SQLite store after it runs.
The problem is that after my background operation completes, the new (or updated) items do not appear in my UITableView. Based on my limited understanding, I believe that I need to notify the main managedObjectContext that changes have occurred so that they may be merged in. My notification is firing, nut no new items appear in the tableview. If I stop the app and restart it, the objects appear in the tableview, leading me to believe that they are being inserted to the core data store successfully but are not being merged into the managedObjectContext being used on the main thread.
I have included a sample of my operation's init, main and notification methods. Am I missing something important or maybe going about this in the wrong way? Any help would be greatly appreciated.
- (id)initWithDelegate:(AppDelegate *)theDelegate
{
if (!(self = [super init])) return nil;
delegate = theDelegate;
return self;
}
- (void)main
{
[self setUpdateContext:[self managedObjectContext]];
NSManagedObjectContext *mainMOC = [self newContextToMainStore];
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
selector:#selector(contextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:updateContext];
[self setMainContext:mainMOC];
// Create/update objects with mainContext.
NSError *error = nil;
if (![[self mainContext] save:&error]) {
DLog(#"Error saving event to CoreData store");
}
DLog(#"Core Data context saved");
}
- (void)contextDidSave:(NSNotification*)notification
{
DLog(#"Notification fired.");
SEL selector = #selector(mergeChangesFromContextDidSaveNotification:);
[[delegate managedObjectContext] performSelectorOnMainThread:selector
withObject:notification
waitUntilDone:YES];
}
While debugging, I examined the notification object that is being sent in contextDidSave: and it seems to contain all of the items that were added (excerpt below). This continues to make me think that the inserts/updates are happening correctly but somehow the merge is not being fired.
NSConcreteNotification 0x6b7b0b0 {name = NSManagingContextDidSaveChangesNotification; object = <NSManagedObjectContext: 0x5e8ab30>; userInfo = {
inserted = "{(\n <GCTeam: 0x6b77290> (entity: GCTeam; id: 0xdc5ea10 <x-coredata://F4091BAE-4B47-4F3A-A008-B6A35D7AB196/GCTeam/p1> ; data: {\n changed =
The method that receives your notification must indeed notify your context, you can try something like this, which is what I am doing in my application:
- (void)updateTable:(NSNotification *)saveNotification
{
if (fetchedResultsController == nil)
{
NSError *error;
if (![[self fetchedResultsController] performFetch:&error]) {
//Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
exit(-1); // Fail
}
}
else
{
NSManagedObjectContext *context = [fetchedResultsController managedObjectContext];
// Merging changes causes the fetched results controller to update its results
[context mergeChangesFromContextDidSaveNotification:saveNotification];
// Reload your table view data
[self.tableView reloadData];
}
}
Hope that helps.
Depending on the specifics of what you are doing, you may be going about this the wrong way.
For most cases, you can simply assign a delegate using NSFetchedResultsControllerDelegate. You provide an implementation for one of the methods specified in "respondingToChanges" depending on your needs, and then send the tableView a reloadData message.
The answer turned out to be unrelated to the posted code which ended up working as I expected. For reasons that I am still not entirely sure of, it had something to do with the first launch of the app. When I attempted to run my update operation on launches after the Core Data store was created, it worked as expected. I solved the problem by pre-loading a version of the sqlite database in the app so that it did not need to create an empty store on first launch. I wish I understood why this solved the problem, but I was planning on doing this either way. I am leaving this here in the hope that someone else may find it useful and not lose as much time as I did on this.
I've been running into a similar problem in the simulator. I was kicking off an update process when transitioning from the root table to the selected folder. The update process would update CoreData from a web server, save, then merge, but the data didn't show up. If I browsed back and forth a couple times it would show up eventually, and once it worked like clockwork (but I was never able to get that perfect run repeated). This gave me the idea that maybe it's a thread/event timing issue in the simulator, where the table is refreshing too fast or notifications just aren't being queued right or something along those lines. I decided to try running in Instruments to see if I could pinpoint the problem (all CoreData, CPU Monitor, Leaks, Allocations, Thread States, Dispatch, and a couple others). Every time I've done a "first run" with a blank slate since then it has worked perfectly. Maybe Instruments is slowing it down just enough?
Ultimately I need to test on the device to get an accurate test, and if the problem persists I will try your solution in the accepted answer (to create a base sql-lite db to load from).
In my Core Data model, I've got a relationship called listItems which links to several listItem entities, each with a stringValue attribute. I've created a control which is essentially a list of NSTextFields, one for each list item. The control is bound to listItems properly, and I've set it up so that pressing the return key creates a new field directly under the currently-edited one and changes the focus to the new field. So, essentially, to add a new item, the user presses Return.
Likewise, if the user ends editing and the currently-edited field is empty, the field is removed (as in, empty fields only appear during "edit mode", so to speak). This works pretty well. Basically, in my listItem NSManagedObject subclass, I do the following:
// Don't allow nil values
if (!value && [[self.recipe ingredients] count] > 1) {
for (EAIngredientRef *ingredient in [self.recipe ingredients]) {
if ([[ingredient sortIndex] integerValue] > [[self sortIndex] integerValue]) {
[ingredient setSortIndex:[NSNumber numberWithInteger:([[ingredient sortIndex] integerValue]-1)]];
}
}
[[self managedObjectContext] deleteObject:self];
return;
}
// Code to handle if it is a real value
The problem I am encountering is that each time a row is deleted this way, it registers with the undoManager. Thus, if I edit a row, press Return (which creates a new row), and click away to end editing, the row disappears. However, if I then undo, the empty field reappears. My goal is to have delete operations involving empty fields be ignored by the undoManager.
How would I go about this? I've tried using [[[self managedObjectContext] undoManager] disableUndoRegistration] and the associated enableUndoRegistration in several spots (such as -didTurnIntoFault, but I suspect that the undo registration might be happening prior to that method)
If you dive more deeply into the Core Data docs, you'll find this tidbit hidden away:
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] disableUndoRegistration];
// Do your work
[[self managedObjectContext] processPendingChanges];
[[[self managedObjectContext] undoManager] enableUndoRegistration];
Changes are not registered with the undo manager normally until the end of the event loop, and so were being registered after you'd turned undo registration back on. The above forces it to occur when you want.