How can I remove all NSTableColumns from an NSTableView? - objective-c

I am trying to implement a method to clear the NSTableView of all items AND columns. But I get a crash when I try to implement the following:
- (void)clearResultData
{
[resultArray removeAllObjects];
NSArray *tableCols = [resultTableView tableColumns];
if ([tableCols count] > 0)
{
id object;
NSEnumerator *e = [tableCols objectEnumerator];
while (object = [e nextObject])
{
NSTableColumn *col = (NSTableColumn*)object;
[resultTableView removeTableColumn:col];
}
}
[resultTableView reloadData];
}

Well, if it's any help you can remove all the columns like this:
- (void)removeAllColumns
{
while([[tableView tableColumns] count] > 0) {
[tableView removeTableColumn:[[tableView tableColumns] lastObject]];
}
}

The NSArray returned by tableColumns is changed by removeTableColumn. Do not assume it is unchanged.
Although it is returned as a non-mutable NSArray, the underlying implementation is being modified and it is not safe to use NSEnumerator with collections that are modified. In the while loop, you are sending a nextObject message to an enumerator whose current object was just deleted -- so bad things can happen!
Here's a more efficient implementation:
NSTableColumn* col;
while ((col = [[tableView tableColumns] lastObject])) {
[tableView removeTableColumn:col];
}
When there are no columns in the table view: tableColumns returns an empty array, lastObject on an empty array returns nil, col is assigned the value of nil, the condition is false and the while loop finishes.

[[[_tableView tableColumns] copy] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[_tableView removeTableColumn:obj];
}];

Here is a Swift implementation:
tableView.tableColumns.forEach({tableView.removeTableColumn($0)})

Related

Implementing copy method - copy whole row

I am trying to copy the row of the NSTableView on clipboard. Here is my code:
- (void) copy:(id)sender
{
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
// I get warning in the line bellow, unused variable changeCount
NSInteger changeCount = [pasteboard clearContents];
NSInteger row = [self.customersViewController.customersTableView selectedRow];
NSTableColumn *columnFirstName = [self.customersViewController.customersTableView tableColumnWithIdentifier:#"firstName"];
NSCell *cellFirstName = [columnFirstName dataCellForRow:row];
NSArray *objectsToCopy = #[[cellFirstName stringValue]];
// I get warning in the line bellow unused variable OK
BOOL OK = [pasteboard writeObjects:objectsToCopy];
}
This code works, and if I select the row in the NSTableView, the content of the firstName column of the selected row is indeed on the pasteboard (I can paste the value in text editor).
However this code have couple of issues:
1. I get 2 warnings as you can see from my comments.I rewrite the code to get rid of the warnings like this. Is anything wrong with the way how I re-write the code?
// warning one
NSInteger changeCount = 0;
changeCount = [pasteboard clearContents];
// warning 2
BOOL OK = NO;
OK = [pasteboard writeObjects:objectsToCopy];
In the code above I name specific which NSTableView I use
...self.customersViewController.customersTableViev....
However If the user switch the view, it may use some other NSTableView...how can I find out from which NSTableView the copy method should copy the row?
If I comment the line where I use specific NSTableView and try to use sender, my app crashes.
//NSInteger row = [self.customersViewController.customersTableView selectedRow];
NSInteger row = [sender selectedRow];
3.How could I write a loop to get all column names instead of specifically write them by hand one by one? I will not know which NSTableView is used anyway....
NSTableColumn *columnFirstName = [self.customersViewController.customersTableView tableColumnWithIdentifier:#"firstName"];
If you don't want the return value you can omit it.
To make you code table view independent you can use firstResponder of the window. Alternatively you can implement copy: in a cubclass of NSTableView. sender is the menu item.
NSTableView's property tableColumns is an array of NSTableColumn.
Here's what I did:
- (void)copy:(id)sender {
NSResponder *firstResponder = self.window.firstResponder;
if (firstResponder && [firstResponder isKindOfClass:[NSTableView class]]) {
NSTableView *tableView = (NSTableView *)firstResponder;
NSArrayController *arrayController = [[tableView infoForBinding:NSContentBinding] objectForKey:NSObservedObjectKey];
// create an array of the keys and formatters of the columns
NSMutableArray *keys = [NSMutableArray array];
for (NSTableColumn *column in [tableView tableColumns]) {
NSString *key = [[column infoForBinding:NSValueBinding] objectForKey:NSObservedKeyPathKey]; // "arrangedObjects.name"
if (key) {
NSRange range = [key rangeOfString:#"."];
if (range.location != NSNotFound)
key = [key substringFromIndex:range.location + 1];
NSFormatter *formatter = [[column dataCell] formatter];
if (formatter)
[keys addObject:#{#"key":key, #"formatter":formatter}];
else
[keys addObject:#{#"key":key}];
}
}
// create a tab separated string
NSMutableString *string = [NSMutableString string];
for (id object in [arrayController selectedObjects]) {
for (NSDictionary *dictionary in keys) {
id value = [object valueForKeyPath:dictionary[#"key"]];
if (value) {
NSFormatter *formatter = [dictionary objectForKey:#"formatter"];
if (formatter)
[string appendFormat:#"%#\t", [formatter stringForObjectValue:value]];
else
[string appendFormat:#"%#\t", value];
}
else
[string appendFormat:#"\t"];
}
[string replaceCharactersInRange:NSMakeRange([string length] - 1, 1) withString:#"\n"];
}
NSPasteboard *pasteboard = [NSPasteboard generalPasteboard];
[pasteboard clearContents];
[pasteboard setString:string forType:NSPasteboardTypeString];
}
}

NSArray crashes the app when accessing it

I have a problem with accessing/setting an object from/to a NSArray returned with the CFPreferencesCopyAppValue() method. My app crashes in this case whereas when I alloc/init it myself, everything works well.
CFArrayRef cfArray;
if ((cfArray = (CFArrayRef)CFPreferencesCopyAppValue(CFSTR("buttonsOrder"), appID))) {
NSArray *castedArray = [(NSArray *)cfArray retain];
NSLog(#"castedArray : %#", castedArray);
buttonsOrder = [castedArray mutableCopy];
NSLog(#"buttonsOrder : %#", buttonsOrder);
CFRelease(cfArray);
[castedArray release];
castedArray = nil;
}
else {
buttonsOrder = [[NSMutableArray alloc] init];
for (NSMutableDictionary *info in togglesInfo) {
[buttonsOrder addObject:[info objectForKey:#"buttonIdentifier"]];
}
}
PS : NSLog() shows me that CFArray is returned well and is casted to NSArray and then NSMutableArray well too.
Any idea ?
Edit :
Here is how I modofy the array :
- (void) tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = [fromIndexPath row];
NSUInteger toIndex = [toIndexPath row];
if (fromIndex == toIndex)
return;
NSString *movedButtonId = [[[buttonsOrder objectAtIndex:fromIndex] retain] autorelease];
[buttonsOrder removeObjectAtIndex:fromIndex];
[buttonsOrder insertObject:movedButtonId atIndex:toIndex];
}
If you crash while trying to add an object to a mutable array, that usually means you're attempting to add a nil object. The only place (in your code snippet above) where I see you adding anything to your mutable array is in the case where you didn't get a valid "cfArray" from CFPreferences. You should make sure "[info objectForKey:#"buttonIdentifier"]" isn't returning nil.
Check to make sure you're not throwing an exception. Or if that's not it, say what your crash really is (it'll say in the Console log of Xcode).

NSNull handling for NSManagedObject properties values

I'm setting values for properties of my NSManagedObject, these values are coming from a NSDictionary properly serialized from a JSON file. My problem is, that, when some value is [NSNull null], I can't assign directly to the property:
fight.winnerID = [dict objectForKey:#"winner"];
this throws a NSInvalidArgumentException
"winnerID"; desired type = NSString; given type = NSNull; value = <null>;
I could easily check the value for [NSNull null] and assign nil instead:
fight.winnerID = [dict objectForKey:#"winner"] == [NSNull null] ? nil : [dict objectForKey:#"winner"];
But I think this is not elegant and gets messy with lots of properties to set.
Also, this gets harder when dealing with NSNumber properties:
fight.round = [NSNumber numberWithUnsignedInteger:[[dict valueForKey:#"round"] unsignedIntegerValue]]
The NSInvalidArgumentException is now:
[NSNull unsignedIntegerValue]: unrecognized selector sent to instance
In this case I have to treat [dict valueForKey:#"round"] before making an NSUInteger value of it. And the one line solution is gone.
I tried making a #try #catch block, but as soon as the first value is caught, it jumps the whole #try block and the next properties are ignored.
Is there a better way to handle [NSNull null] or perhaps make this entirely different but easier?
It might be a little easier if you wrap this in a macro:
#define NULL_TO_NIL(obj) ({ __typeof__ (obj) __obj = (obj); __obj == [NSNull null] ? nil : obj; })
Then you can write things like
fight.winnerID = NULL_TO_NIL([dict objectForKey:#"winner"]);
Alternatively you can pre-process your dictionary and replace all NSNulls with nil before even trying to stuff it into your managed object.
Ok, I've just woke up this morning with a good solution. What about this:
Serialize the JSON using the option to receive Mutable Arrays and Dictionaries:
NSMutableDictionary *rootDict = [NSJSONSerialization JSONObjectWithData:_receivedData options:NSJSONReadingMutableContainers error:&error];
...
Get a set of keys that have [NSNull null] values from the leafDict:
NSSet *nullSet = [leafDict keysOfEntriesWithOptions:NSEnumerationConcurrent passingTest:^BOOL(id key, id obj, BOOL *stop) {
return [obj isEqual:[NSNull null]] ? YES : NO;
}];
Remove the filtered properties from your Mutable leafDict:
[leafDict removeObjectsForKeys:[nullSet allObjects]];
Now when you call fight.winnerID = [dict objectForKey:#"winner"]; winnerID is automatically going to be (null) or nil as opposed to <null> or [NSNull null].
Not relative to this, but I also noticed that it is better to use a NSNumberFormatter when parsing strings to NSNumber, the way I was doing was getting integerValue from a nil string, this gives me an undesired NSNumber of 0, when I actually wanted it to be nil.
Before:
// when [leafDict valueForKey:#"round"] == nil
fight.round = [NSNumber numberWithInteger:[[leafDict valueForKey:#"round"] integerValue]]
// Result: fight.round = 0
After:
__autoreleasing NSNumberFormatter* numberFormatter = [[NSNumberFormatter alloc] init];
fight.round = [numberFormatter numberFromString:[leafDict valueForKey:#"round"]];
// Result: fight.round = nil
I wrote a couple of category methods to strip nulls from a JSON-generated dictionary or array prior to use:
#implementation NSMutableArray (StripNulls)
- (void)stripNullValues
{
for (int i = [self count] - 1; i >= 0; i--)
{
id value = [self objectAtIndex:i];
if (value == [NSNull null])
{
[self removeObjectAtIndex:i];
}
else if ([value isKindOfClass:[NSArray class]] ||
[value isKindOfClass:[NSDictionary class]])
{
if (![value respondsToSelector:#selector(setObject:forKey:)] &&
![value respondsToSelector:#selector(addObject:)])
{
value = [value mutableCopy];
[self replaceObjectAtIndex:i withObject:value];
}
[value stripNullValues];
}
}
}
#end
#implementation NSMutableDictionary (StripNulls)
- (void)stripNullValues
{
for (NSString *key in [self allKeys])
{
id value = [self objectForKey:key];
if (value == [NSNull null])
{
[self removeObjectForKey:key];
}
else if ([value isKindOfClass:[NSArray class]] ||
[value isKindOfClass:[NSDictionary class]])
{
if (![value respondsToSelector:#selector(setObject:forKey:)] &&
![value respondsToSelector:#selector(addObject:)])
{
value = [value mutableCopy];
[self setObject:value forKey:key];
}
[value stripNullValues];
}
}
}
#end
It would be nice if the standard JSON parsing libs had this behaviour by default - it's almost always preferable to omit null objects than to include them as NSNulls.
Another method is
-[NSObject setValuesForKeysWithDictionary:]
In this scenario you could do
[fight setValuesForKeysWithDictionary:dict];
In the header NSKeyValueCoding.h it defines that "Dictionary entries whose values are NSNull result in -setValue:nil forKey:key messages being sent to the receiver.
The only downside is you will have to transform any keys in the dictionary to keys that are in the receiver. i.e.
dict[#"winnerID"] = dict[#"winner"];
[dict removeObjectForKey:#"winner"];
I was stuck with the same problem, found this post, did it in a slightly different way.Using category only though -
Make a new category file for "NSDictionary" and add this one method -
#implementation NSDictionary (SuperExtras)
- (id)objectForKey_NoNSNULL:(id)aKey
{
id result = [self objectForKey:aKey];
if(result==[NSNull null])
{
return nil;
}
return result;
}
#end
Later on to use it in code, for properties that can have NSNULL in them just use it this way -
newUser.email = [loopdict objectForKey_NoNSNULL:#"email"];
Thats it

View-based NSTableView filtering + animation

I have a view based NSTableView that I sometimes filter using NSPredicate. Is there any way to animate the items being removed/added/reordered throughout the tableview to have the same effect as beginUpdates, endUpdates and insertRowsAtIndexes:withAnimation, etc?
I've explored ways such as manually filtering out my array but my attempts proved to be futile so now I am wondering if there is a better (or built in way) to do this. I have wondered if NSArrayController does this automatically but I don't think it does.
I've written code to do this myself - given 'before' and 'after' arrays, compute the required parameters to insertRowsAtIndexPaths:, deleteRowsAtIndexPaths:, etc. The code is a bit fiddly so probably has bugs - use at your discretion!
#interface NSArray (ArrayDifference)
- (void) computeDifferenceTo:(NSArray *)newArray returningAdded:(NSMutableArray **)rowsAdded andDeleted:(NSMutableArray **)rowsDeleted;
#end
#implementation NSArray (ArrayDifference)
// Given two arrays that are expected have items added or removed but not re-ordered, compute the differences
// in a way usable for UITable insertRows and deleteRows
- (void) computeDifferenceTo:(NSArray *)newArray returningAdded:(NSMutableArray **)rowsAdded andDeleted:(NSMutableArray **)rowsDeleted
{
NSArray *oldArray = self;
*rowsAdded = [[[NSMutableArray alloc] init] autorelease];
*rowsDeleted = [[[NSMutableArray alloc] init] autorelease];
NSUInteger oldCount = [oldArray count];
NSUInteger newCount = [newArray count];
// Step through the two arrays
NSInteger oldIndex = 0, newIndex=0;
for (; newIndex < newCount && oldIndex < oldCount; )
{
id newItem = [newArray objectAtIndex:newIndex];
id oldItem = [oldArray objectAtIndex:oldIndex];
// If the two objects match, we step forward on both sides
if (newItem == oldItem) {
++newIndex;
++oldIndex;
}
else {
// Look for the old item to appear later in the new array, which would mean we have to add the rows in between
NSRange range = { newIndex+1, newCount - newIndex-1 };
NSUInteger foundIndex = [newArray indexOfObject:oldItem inRange:range];
if (foundIndex != NSNotFound)
for (; newIndex < foundIndex; ++newIndex)
[*rowsAdded addObject:[NSIndexPath indexPathForRow:newIndex inSection:0]];
else {
// Look for the new item to appear later in the old array, which would mean we have to remove the rows in between
NSRange range = { oldIndex+1, oldCount - oldIndex-1 };
NSUInteger foundIndex = [oldArray indexOfObject:newItem inRange:range];
if (foundIndex != NSNotFound)
for (; oldIndex < foundIndex; ++oldIndex)
[*rowsDeleted addObject:[NSIndexPath indexPathForRow:oldIndex inSection:0]];
else {
// Old item must be removed and new item added, then we carry on
[*rowsAdded addObject:[NSIndexPath indexPathForRow:newIndex++ inSection:0]];
[*rowsDeleted addObject:[NSIndexPath indexPathForRow:oldIndex++ inSection:0]];
}
}
}
}
// Once the loop is finished, add in what's left in the new array and remove what is left in the old array
for (; newIndex < newCount; ++newIndex)
[*rowsAdded addObject:[NSIndexPath indexPathForRow:newIndex inSection:0]];
for (; oldIndex < oldCount; ++oldIndex)
[*rowsDeleted addObject:[NSIndexPath indexPathForRow:oldIndex inSection:0]];
}
#end
Then you call it like this:
NSMutableArray *rowsAdded=nil, *rowsDeleted=nil;
[myArray computeDifferenceTo:newArray returningAdded:&rowsAdded andDeleted:&rowsDeleted];
[myTableView beginUpdates];
[myTableView insertRowsAtIndexPaths:rowsAdded withRowAnimation:UITableViewRowAnimationBottom];
[myTableView deleteRowsAtIndexPaths:rowsDeleted withRowAnimation:UITableViewRowAnimationFade];
[myTableView endUpdates];

Categories Obj-C, NestedArrays

I'm reading an example in More Iphone Development book in the Core Data section, and the author creates a category to turn the NSIndexPath into its row key and row label. Here's the code:
#implementation NSArray(NestedArrays)
- (id)nestedObjectAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger row = [indexPath row];
NSUInteger section = [indexPath section];
NSArray *subArray = [self objectAtIndex:section];
if (![subArray isKindOfClass:[NSArray class]])
return nil;
if (row >= [subArray count])
return nil;
return [subArray objectAtIndex:row];
}
- (NSInteger)countOfNestedArray:(NSUInteger)section {
NSArray *subArray = [self objeectAtIndex:section];
return [subArray count];
}
#end
In the first method, after he gets the row and section for the NSIndexPath, I'm not sure what's going on afterwards. I don't see why he creates a new array at the section, and then I don't understand the reason behind the two if statements that follow. Thanks in advance.
The code does not actually create a new array, the author is simply holding a reference to an object that is already stored in self. Annotations for each line are below.
Get the row from the indexPath:
NSUInteger row = [indexPath row];
Get the section index from the indexPath:
NSUInteger section = [indexPath section];
Dereference the array within self at index section
NSArray *subArray = [self objectAtIndex:section];
If the sub array is not really an array, fail:
if (![subArray isKindOfClass:[NSArray class]])
return nil;
If the row within the sub array is greater than its last index, fail:
if (row >= [subArray count])
return nil;
Return the object in the sub array at the given row:
return [subArray objectAtIndex:row];
Maybe it would help if some comments were included:
// Get the subarray corresponding to the requested section
NSArray *subArray = [self objectAtIndex:section];
// verify that it is really an NSArray
if (![subArray isKindOfClass:[NSArray class]])
return nil; // not an array - fail
// Check the array subscript (row #) against the size of the subarray
if (row >= [subArray count])
return nil; // out of bounds - fail
return [subArray objectAtIndex:row]; // return the requested element
He doesn't create a new array. He get's the "sub" array that represents the section.
The first if statements is there because you can create a NSArray that (let's say) is full of NSStrings. And if the subItem would be a NSString, objectAtIndex: would raise an exception.
And the second one protects you when you try access an item that is on a index that is not in the subArray. If you wouldn't return nil at this position there would be an exception too.
First, he is not creating a new array. He is just taking object for specified indexPath and checks weather it is array. If it is not array, or specified row is larger then array's size he returns nil.
He gets the row and the section, and then he gets the array specified by that section. Then he does two sanity checks:
Is thing I've got actually an array?
Is the row number valid for this particular array?
If both of these are true, he returns the object from the section array at the index specified by row.