I have some calculated values in the core data database that I need to update just before saving. Basically I'm numbering some entities in order to ease up the navigation between them.
Currently I'm observing NSManagedObjectContextWillSaveNotification and trying to do this numbering there. It would seem that the changes that I make are saved but undo manager still seems to have some modifications. This makes the document look like it has changes (mark on the close button) even though managed object context says that it doesn't have (hasChanges). If I undo once, the document looks like it has no changes but in turn, the managed object context does.
Does the undo manager somehow reset itself in the wrong place or am I doing something wrong?
Update
The somewhat obfuscated code in which I'm doing this renumbering looks like this:
- (void)managedObjectContextWillSave:(NSNotification *)notification
{
// Force the content view controller to save any pending changes.
[_contentViewController saveChanges];
NSArray *itemSortDesc = [self sortDescriptorsForSomeItem];
NSArray *items = [SomeItem findAllObjectsInContext:self.managedObjectContext
andSortBy:itemSortDesc];
NSUInteger i = 0;
for (SomeItem *i in items)
{
i.uid = [NSNumber numberWithUnsignedInteger:i++];
}
}
The _contentViewController contains a text field that will be parsed in to multiple instances of SomeItem.
I'm guessing that your numbering affects the undo stack.
I would probably try to handle this in NSManagedObject willSave instead of using NSManagedObjectContextWillSaveNotification but I suspect that won't solve your problem.
You could try this:
[[self.managedObjectContext undoManager] disableUndoRegistration];
// do the renumbering
[self.managedObjectContext processPendingChanges];
[[self.managedObjectContext undoManager] enableUndoRegistration];
I use this to avoid dirtying a brand new document during initialization. I'm not certain it will work correctly for saving, but it might be worth trying.
Related
I'm trying to release some strain on a view-based NSOutlineView for which I changed a single item property and which I initially reloaded just fine using [myOutlineView reloadData].
I tried [myOutlineView reloadItem: myOutlineViewItem] but it never calls - (NSView *)outlineView:(NSOutlineView *)ov viewForTableColumn:(NSTableColumn *)tableColumn item:(id)item and consequently the data is not updated.
-(void)reloadOutlineViewObject
{
//[myOutlineView reloadData]; //Reload data just fines but is ressource-hungry
NSLog(#"%d",[myOutlineView rowForItem:myOutlineViewItem]; //Making sure my object is an item of the outlineView, which it is !
[myOutlineView reloadItem:myOutlineViewItem];
}
Am I missing something here ?
UPDATE
As pointed out in the comments, my outlineView is view-based.
UPDATE 2
Trying out some stuffs made me realized that the object I am reloading is a second-level object (cf object tree) and calling reloadItem:firstLevelObject reloadChildren:YES does work.
Would it be possible that we can only call reloadItem: on first-level object ? That would be highly inefficient in my case (I only have one two level item and plenty of second level) !
nil ->firstLevelA ->secondLevel1
->secondLevel2
->firstLevelB ->secondLevel3
->secondLevel4
Gonna try to subclass NSOutlineView and rewrite reloadItem: in the mean time.
UPDATE 3
I took a look at NSOutlineView in Cocotron to get start and felt that the code I needed to write to overwrite reloadItem would be quiet heavy. Anyone to confirm ?
I encountered this same problem with a view-based outline view, where calling -reloadItem: seems to just not do anything. This definitely seems like a big bug, though the documentation doesn't explicitly say that reloadItem will reacquire the views for that row.
My workaround was to call NSTableView's -reloadDataForRowIndexes:columnIndexes: instead, which seems to work as expected, triggering a call to the -outlineView:viewForTableColumn:item: delegate method for just that item. You can get the row that needs to be reloaded by calling -rowForItem: and passing in the item you want to reload.
This really isn't a bug - it was something I had explicitly designed. My thought was that reloadItem (etc) should just reload the outline view item properties, not the table cell at that item, since it doesn't carry enough specific information on what to reload (such as what specific cell you might want reloaded). I had intended for people to use reloadDataForRowIndexes:columnIndexes: to reload a particular view based tableview cell; we usually don't provide cover methods when the base class can easily do the same thing with just a few lines of code.
However, having said that, I know multiple people have gotten confused about this, and most people expect it to reload the cell too.
Please log a bug requesting Apple to change this.
thanks,
-corbin
Apple seems to have "fixed" it.
WWDC 2016, presentation 203 "What's New in Cocoa" at 30:35 in the video:
"NSOutlineView
Reloads cell views associated with the 'item' when reloadItem() is called"
reloadItem: works only on macOS 10.12.
From release notes:
https://developer.apple.com/library/content/releasenotes/AppKit/RN-AppKit/
NSOutlineView will now reload the cell views associated with ‘item’
when [outlineView reloadItem:] is called. The method simply calls
[outlineView reloadDataForRowIndexes:columnIndexes:] passing the
particular row that is to be reloaded, and all the columns. For
compatibility, this will only work for applications that link against
the 10.12 SDK.
So, if you want to reload row on earlier systems, you should use -reloadDataForRowIndexes:columnIndexes:.
Something like that:
let index = outlineView.row(forItem: obj)
let rowIndex = IndexSet(integer: index)
let cols = IndexSet(0 ... outlineView.numberOfColumns)
outlineView.reloadData(forRowIndexes: rowIndex, columnIndexes: cols)
I have troubles with NSArrayController rearrangeObjects function - this function called from some background treads and sometimes App crashes with error: 'endUpdates called without a beginUpdates'.
How can i detect if arrayController is rearranging objects in current moment and add next rearrange to some like queue or cancel current rearranging and run new?
May be there is another solution for this problem?
Edit code added:
TableController implementation:
- (void)setContent{//perfoms on main thread
//making array of content and other functions for setting-up content of table
//...
//arrayController contains objects of MyNode class
//...
//end of setting up. Call rearrangeObjects
[arrayController rearrangeObjects];
}
- (void)updateItem:(MyNode *)sender WithValue:(id)someValue ForKey:(NSString *)key{
[sender setValue:someValue forKey:key];
[arrayController rearrangeObjects];//exception thrown here
}
MyNode implementation:
- (void)notifySelector:(NSNotification *)notify{
//Getted after some processing finished
id someValue = [notify.userInfo objectForKey:#"observedKey"];
[delegate updateItem:self WithValue:someValue ForKey:#"observedKey"];
}
Don't do that. AppKit (to which NSArrayController belongs) is not generally thread safe. Instead, use -performSelectorOnMainThread:... to update your UI (including NSArrayController). ALWAYS do updating on the main thread.
Joshua and Dan's solution is correct. It is highly likely that you are performing operations on your model object in a background thread, which then touches the array controller, and hence touches the table.
NSTableView itself is not threadsafe. Simply adding in a "beginUpdates/endUpdates" pair will just avoid the race condition for a bit. However, like Fin noted, it might be good to do the pair of updates for performance reasons, but it won't help with the crash.
To find the sources of the crash, add some assertions in your code on ![NSThread currentThread].mainThread -- particularly any places before you touch the array controller's content. This will help you isolate the problem. Or, subclass NSTableView and add the assertion in somewhere key, like overriding -numberOfRows (which is called frequently on changes), and calling super.
-corbin
AppKit/NSTableView
I solved this by adding this at the very start of my UI initialisation
[myTableView beginUpdates];
and then at the end after the persistant store has been loaded fully:
[myTableView endUpdates];
This also make the app startup a lot better since it does not constantly have to reload all loads from the MOC.
i'm building an app in Xcode where there are a 81 textviews in the NIB, each with a sequential name, so box1, box2, box3, box4 etc.
When doing data manipulation i want to be able to use the data in each box to add to an array for example. What i would like to be able to do is put this in a loop, so for example something like:
NSMutableArray *array = [[NSMutableArray alloc] init];
for (int i=1; i<82; i++) {
[array addObject: [Indirect("box" & i).text];
}
Similarly when outputting back to the textviews, i want to be able to loop from the array rather than referring to each textview independently. so something like:
for (int i=1; i<82; i++) {
indirect("box" & i).text = [array objectAtIndex:i];
}
Any ideas? Sorry if this is obvious - fairly new to the game.
Consider the MVC design pattern. Your calculation shouldn't be based directly off the views (the UI) but rather off some state in the controller, which is set by the views. Each time a field is edited, it notifies your controller via target/action or via Cocoa Bindings. When that happens, the controller updates your data model (in your case, that means it updates the computation and probably reflects the result in another part of the UI - the "total" field).
In Cocoa, there are two ways to do it:
Add all of the fields to an array in awakeFromNib. Enjoy writing 82 addObject: messages.
Remove the fields from the nib and create them in a loop in code, adding each one to an array. (This is what I'd do.)
Once they're in an array, you can refer to them by index, same as you do with the strings.
But you mention that you're accessing the fields' text property. This only exists in Cocoa Touch, not in Cocoa. If you're using Cocoa Touch, then you have a third option:
Replace your 82 outlets with an outlet collection.
The value of an outlet collection property is an array, so you get to create your fields in the nib but still refer to them by index into the array in the code.
On the other hand, I'd probably still create them in code, even though I'm more pro-nib than most Cocoa Touch devs. Part of it is habit (I'm still almost entirely a Mac developer), but part of it is the DRY principle. If I create the fields in a loop in code, I can describe all of the fields exactly once, along with the ways in which they differ. I won't have the risk of changing one field and forgetting (or even just having) to update the others, or of going to change all the fields (again) and forgetting to change one.
I would handle this using the tags: you can set them from 1 to 81 in the nib (look for the field under Control).
Then in -awakeFromNib you can call [self viewWithTag:i] inside a for loop.
It's definitely less work than individual outlets, and I think even simpler than an outlet collection – filling in the number means you don't have to connect outlets for all the text fields.
I'm changing a cocoa binding programatically. I'm binding a NSTextField's value to the selection of an ArrayController. After I manually change the binding, I'm getting the "not key-value coding compliant for the key.." error, with the key being the old key, not the new one.
Check out the code:
NSTextField *textField = [self listTextField];
NSDictionary *currentBindInfo = [textFieldTableViewCell infoForBinding:NSValueBinding];
NSLog(#"pre-change bindings for textField: %#", currentBindInfo);
/* Change the binding. [Tried unbind: first, no difference] */
[textField bind:NSValueBinding
toObject:[currentBindInfo valueForKey:NSObservedObjectKey]
withKeyPath:#"objectValue.iLifeProductName"
options:[currentBindInfo valueForKey:NSOptionsKey]];
/* Log the info so we can confirm it changed. debugging. */
NSLog(#"post-change bindings for textField: %#", [textFieldTableViewCell infoForBinding:NSValueBinding]);
To troubleshoot, I call 'infoForBinding' before and after the change and it looks to be changed correctly. I can see the old value, then I call bind:toObject... and dump the infoForBinding a second time, and the value has changed for the binding:
2011-07-06 22:36:23.137 My App 2011[14640:407] pre-change bindings for listTextFieldTableViewCell: {
NSObservedKeyPath = "selection.osxProductName";
NSObservedObject = "...sameTextField... 0x4009cc380>";
NSOptions = {...same... };
}
2011-07-06 22:36:23.138 My App 2011[14640:407] post-change bindings for listTextFieldTableViewCell: {
NSObservedKeyPath = "selection.iLifeProductName";
NSObservedObject = "...sameTextField... 0x4009cc380>";
NSOptions = {...same... };
}
But the code is still calling the original key:
2011-07-06 22:36:23.231 My App 2011[14640:407] [ valueForUndefinedKey:]: the entity ILifeVersion is not key value coding-compliant for the key "osxProductName".
--
The NSArrayController is bound to a ManagedObjectContext, the entity name is being changed earlier with this:
[[self listAC] setEntityName:entityName];
Is the original keyValuePath being cached somewhere that I need to clear out? Is there a message like willChange/didChangeValueForKeyValuePath that I need to send to the binding or arrayController when I change the observed keypath?
Ideas?
Thanks!
As #noa pointed out, you’re looking at the binding on the cell, but changing the binding on its control. That’s bound (ahem) to cause problems.
Replace this:
[textField bind:NSValueBinding
toObject:[currentBindInfo valueForKey:NSObservedObjectKey]
withKeyPath:#"objectValue.iLifeProductName"
options:[currentBindInfo valueForKey:NSOptionsKey]];
with this:
[textFieldTableViewCell bind:NSValueBinding
toObject:[currentBindInfo valueForKey:NSObservedObjectKey]
withKeyPath:#"objectValue.iLifeProductName"
options:[currentBindInfo valueForKey:NSOptionsKey]];
And see if it works better.
The explanation for this is a bit arcane, and I’m doing it from memory, so please excuse me if I get some of the details wrong.
Because NSControls and their NSCell works so closely together, you can actually bind to either the control or the cell in most instances, and you’ll get very similar results. That is, there’s code in the control to call the proper methods on its NSCell if the control’s been bound to, and vice-versa.
This means that if, in XIB, you bind to one or the other things will work, which is good. It also means you can bind to a cell in cases where you have multiple cells per view, so that’s good. HOWEVER, it can lead to confusion, because in fact you can actually bind to both your view and its cell, and in fact bind them in different ways, and then they’ll crosstalk.
In your example, I believe you’re adding a second binding to the NSControl in addition to the one on its NSCell. You’re doubly-bound. That’s no good.
In terms of best practice, I try to bind only to NSControls unless I have a good reason to drop down to NSCells. Partly because it matches what I do in XIB, partly because any standard helps reduce exactly this problem, and partly because NSCells are being gently deprecated.
I'm using an NSArrayController, NSMutableArray and NSTableView to show a list of my own custom objects (although this question probably applies if you're just showing a list of vanilla NSString objects too).
At various points in time, I need to clear out my array and refresh the data from my data source. However, just calling removeAllObjects on my NSMutableArray object does not trigger the KVO updates, so the list on screen remains unchanged.
NSArrayController has no removeAllObjects method available, which seems really weird. (It does have addObject, which I use to add the objects, ensuring the KVO is triggered and the UI is updated.)
The cleanest way I've managed to cause this happen correctly is:
[self willChangeValueForKey:#"myArray"];
[myArray removeAllObjects];
[self didChangeValueForKey:#"myArray"];
...so I'm kind of having to do the KVO notification manually myself (this is in my test app class, that contains the myArray property, which is NSMutableArray, as mentioned.)
This seems wrong - is there a better way? From my googling it seems a few people are confused by the lack of removeAllObjects in NSArrayController, but haven't seen any better solutions.
I have seen this solution:
[self removeObjectsAtArrangedObjectIndexes:
[NSIndexSet indexSetWithIndexesInRange:
NSMakeRange(0, [[self arrangedObjects] count])]];
but this looks even more unpleasant to me. At least my solution is at least marginally self-documenting.
Did Apple not notice that sometimes people might want to empty a list control being managed via an NSArrayController object? This seems kind of obvious, so I think I must be missing something...
Aside: of course, if I add new items to the array (via NSArrayController), then this triggers a KVO update with the NSArrayController/NSTableView, but:
Sometimes I don't put any items in the list, because there are none. So you just see the old items.
This is a bit yucky anyway.
You don't remove items from a table view. It doesn't have any items—it just displays another object's items.
If you bound the array controller's content array binding to an array property of some other object, then you should be working with that property of that object. Use [[object mutableArrayValueForKey:#"property"] removeAllObjects].
If, on the other hand, you haven't bound the array controller's content array binding, then you need to interact with its content directly. Use [[arrayController mutableArrayValueForKey:#"content"] removeAllObjects]. (You could also work with arrangedObjects instead of content. If one doesn't work, try the other—I've only ever done things the first way, binding the array controller to something else.)
Had this problem as well and solved it this way:
NSArrayController* persons = /* your array controller */;
[[persons content] removeAllObjects];
Swift
#IBOutlet var acLogs: NSArrayController!
acLogs.removeObjects(acLogs.content as! [AnyObject])
worked for me.
Solution in Swift:
if let ac = arrayController
{
let range:NSRange = NSMakeRange(0, ac.arrangedObjects.count);
let indexSet:NSIndexSet = NSIndexSet(indexesInRange: range);
ac.removeObjectsAtArrangedObjectIndexes(indexSet);
}
Just an update that works in Swift 4:
let range = 0 ..< (self.arrayController.arrangedObjects as AnyObject).count
self.arrayController.remove(atArrangedObjectIndexes: IndexSet(integersIn: range))