Implementing A Static NSOutlineView - objective-c

I'm having a hard time scraping together enough snippets of knowledge to implement an NSOutlineView with a static, never-changing structure defined in an NSArray. This link has been great, but it's not helping me grasp submenus. I'm thinking they're just nested NSArrays, but I have no clear idea.
Let's say we have an NSArray inside an NSArray, defined as
NSArray *subarray = [[NSArray alloc] initWithObjects:#"2.1", #"2.2", #"2.3", #"2.4", #"2.5", nil];
NSArray *ovStructure = [[NSArray alloc] initWithObjects:#"1", subarray, #"3", nil];
The text is defined in outlineView:objectValueForTableColumn:byItem:.
- (id)outlineView:(NSOutlineView *)ov objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)ovItem
{
if ([[[tableColumn headerCell] stringValue] compare:#"Key"] == NSOrderedSame)
{
// Return the key for this item. First, get the parent array or dictionary.
// If the parent is nil, then that must be root, so we'll get the root
// dictionary.
id parentObject = [ov parentForItem:ovItem] ? [ov parentForItem:ovItem] : ovStructure;
if ([parentObject isKindOfClass:[NSArray class]])
{
// Arrays don't have keys (usually), so we have to use a name
// based on the index of the object.
NSLog([NSString stringWithFormat:#"%#", ovItem]);
//return [NSString stringWithFormat:#"Item %d", [parentObject indexOfObject:ovItem]];
return (NSString *) [ovStructure objectAtIndex:[ovStructure indexOfObject:ovItem]];
}
}
else
{
// Return the value for the key. If this is a string, just return that.
if ([ovItem isKindOfClass:[NSString class]])
{
return ovItem;
}
else if ([ovItem isKindOfClass:[NSDictionary class]])
{
return [NSString stringWithFormat:#"%d items", [ovItem count]];
}
else if ([ovItem isKindOfClass:[NSArray class]])
{
return [NSString stringWithFormat:#"%d items", [ovItem count]];
}
}
return nil;
}
The result is '1', '(' (expandable), and '3'. NSLog shows the array starting with '(', hence the second item. Expanding it causes a crash due to going 'beyond bounds.' I tried using parentForItem: but couldn't figure out what to compare the result to.
What am I missing?

The example behind the link you included shows an NSDictionary taking care of the subarray stuff, if I'm reading it correctly. So I think your ovStructure should not be an array but a dictionary. But, more fundamentally, I think you should really look into NSTreeController. Unfortunately, NSTreeController is notoriously hard to work with, but improvements were made last year and even I got it working in the end. Good luck.

Related

How does one eliminate Objective-C #try #catch blocks like this?

I'm a developer from Python world used to using exceptions. I found in many places that using exceptions is not so wise here, and did my best to convert to NSErrors when needed. but then I encounter this:
NSMutableArray *results;
for (NSDictionary *dict in dicts)
{
// Memory management code omitted
SomeModel *model = [[SomeModel alloc] init];
model.attr1 = [[dict objectForKey:#"key1"] integerValue];
model.attr2 = [[dict objectForKey:#"key2"] integerValue];
model.attr3 = [[dict objectForKey:#"key3"] integerValue];
model.attr4 = [[dict objectForKey:#"key4"] integerValue];
[results addObject:model];
}
with some of the objects in dict containing NSNull, which would result an "unrecognized selector" exception. In that case, I want to drop that datum completely. My first instinct is to wrap the whole content of the for block into a #try-#catch block:
NSMutableArray *results;
for (NSDictionary *dict in dicts)
{
#try
{
SomeModel *model = [[SomeModel alloc] init];
model.attr1 = [[dict objectForKey:#"key1"] integerValue];
model.attr2 = [[dict objectForKey:#"key2"] integerValue];
model.attr3 = [[dict objectForKey:#"key3"] integerValue];
model.attr4 = [[dict objectForKey:#"key4"] integerValue];
[results addObject:model];
}
#catch(NSException *exception)
{
// Do something
}
}
But is this a good approach? I can't come up with a solution without repeating checks on each variable, which is really ugly IMO. Hopefully there are alternatives to this that haven't occur to me. Thanks in advance.
The proper Objective-C way to do this would be:
for (NSDictionary *dict in dicts)
{
if (! [dict isKindOfClass:[NSDictionary class]])
continue;
// ...
}
Testing if a receiver can respond to a message before sending it is a typical pattern in Objective-C.
Also, take note that exceptions in Objective-C are always a programmer error and are not used for normal execution flow.
Many people use a category on NSDictionary for these cases:
- (id)safeObjectForKey:(id)aKey
{
id obj = [self objectForKey:aKey];
if ([obj isKindOfClass:[NSNull class]])
{
return nil;
}
return obj;
}
You still need to make sure, that your dict is an actual dictionary instance.
In the end I decided to solve the problem using KVC. Something like this:
- (id)initWithPropertyDictionary:(NSDictionary *)dict
lookUpTable:(NSDictionary *)keyToProperty
{
self = [self init];
for (NSString *key in dict)
{
NSString *propertyName;
if ([keyToProperty objectForKey:key])
propertyName = [keyToProperty objectForKey:key];
else
propertyName = key;
if ([[dict objectForKey:key] isKindOfClass:[NSNull class]])
{
[self release];
return nil;
}
else
{
[self setValue:[dict objectForKey:key] forKey:propertyName];
}
}
}
The setback of this resolution is that I'll have to use NSNumber for my properties, but for JSON data there is really no distinction between floating numbers and integers, so this is fine.
And if you really want primitive types, you can couple this method with custom setters that converts those NSNumbers into appropriate types.
With this, all you need to do is check for nil before adding the object into the array. Much cleaner everywhere except the model class.
Thanks to jaydee3 for inspiring me to focus on changing the model class.

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

UISearchBar and NSDictionary

i want to add to my app a UISearchBar.
I have 3 NSDictionary that contains respectively 3 arrays (sorted). The 3 NSDictionaries have the same key (#"Elements"), stored in a NSMutableArray called listObjects.
At the end of the 3 dicts i have this situation:
[listObjects addObject:dictOne];
[listObjects addObject:dictTwo];
[listObjects addObject:dictThree];
I also have a filteredListContent to store the arrays (for quick searching).
filteredListContent = [[NSMutableArray alloc] initWithCapacity: [listObjects count]];
[filteredListContent addObjectsFromArray: listObjects];
Now, the first question is: does the filteredListContent array store all the 3 dict keys from listObjects?
Then.
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope{
[self.filteredListContents removeAllObjects];
NSString *cellTitle;
for (cellTitle in listObjects){
NSComparisonResult result = [cellTitle compare:searchText options:NSCaseInsensitiveSearch range:NSMakeRange(0, [searchText length])];
if (result == NSOrderedSame){
[filteredListContents addObject:cellTitle];
}
}
}
I got a crash as i begin typing in
NSComparisonResult result = [cellTitle compare:searchText options:NSCaseInsensitiveSearch range:NSMakeRange(0, [searchText length])];`
line.
Is there supposed to be the key of the dict (#"Elements"), in order to search in all the 3 arrays or?
Edit
Full code
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSDictionary *dictionary = [listObjects objectAtIndex:section];
NSArray *array = [dictionary objectForKey:#"Elements"];
return [array count];
}
- (void)filterContentForSearchText:(NSString*)searchText scope:(NSString*)scope{
NSString *cellTitle;
for (NSDictionary *dict in listaObjects)
{
NSArray *elements = [dict objectForKey: #"Elements"];
for (NSString *cellTitle in elements)
if ([cellTitle compare: searchText options: NSCaseInsensitiveSearch range: NSMakeRange(0, [searchText length])] = NSOrderedSame)])
[filteredListContent addObject: cellTitle];
}}
#pragma mark -
#pragma mark UISearchDisplayController Delegate Methods
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString{
[self filterContentForSearchText:searchString scope:
[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:[self.searchDisplayController.searchBar selectedScopeButtonIndex]]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchScope:(NSInteger)searchOption{
[self filterContentForSearchText:[self.searchDisplayController.searchBar text] scope:
[[self.searchDisplayController.searchBar scopeButtonTitles] objectAtIndex:searchOption]];
// Return YES to cause the search result table view to be reloaded.
return YES;
}
I got it working, but it shows the contents of all the arrays, even if i type a specific word. What am i doing wrong?
I don't get it. You first add the three dictionaries to filteredListContents and then you remove all objects from the array? That means the array is totally empty again.
Then you enumerate listObjects, which only contains 3 dictionaries, and no strings, using an NSString enumerator. That doesn't make sense.
First, there is no need to do:
[filteredListContents addObjectsFromArray: listObjects];
So remove that line. Now do something like:
for (NSDictionary *dict in listObjects)
{
NSArray *elements = [dict objectForKey: #"Elements"];
for (NSString *cellTitle in elements)
if (cellTitle compare: searchText options: options: NSCaseInsensitiveSearch range: NSMakeRange(0, [searchText length])] = NSOrderedSame)
[filteredListContents addObject: cellTitle];
}
OK, that gives you an array of equal strings. Now what? I'd expect you to collect the cells with that title, but you are not doing that here. You could, instead of storing strings, store dictionaries with the cell title as key and the cell as value. That would make the loop only one level deeper.
As someone wiser than me once said: Show me your problem, not your solution. Then I can help you.
The full code doesn't make any sense to me, sorry. What are you trying to collect actually?

argument isKindOfClass: [NSNumber class] - sane way to check this?

So I was playing with something where the class type of the arg is unknown until runtime.
like this:
- (NSNumber *)doWhatever:(id)arg
{
// this ALWAYS FAILS
if ([arg isKindOfClass:[NSNumber class]]) {
return arg;
}
else {
// what was it???
NSLog("arg klass=%#", [arg class]); // prints NSCFNumber
}
// This check works correctly.
if ([arg isKindOfClass:[NSArray class]]) {
for (id x in arg) {
NSNumber *result = [self doWhatever:x];
if (result) {
return result;
}
}
}
return nil;
}
- (void)someMethod
{
NSArray *myArray = [NSArray arrayFromObjects:[NSNumber numberWithInt:3], nil]];
NSNumber *myNum = [self doWhatever:myArray];
NSLog(#"myNum=%#", myNum);
}
This is obviously a contrived example of what I'm trying to do.
The point is this never works b/c the class of "arg" always appears as NSCFNumber, and I can't figure out a way to check for that.
Any way to make it less confusing to detect whether an arbitrary value in an array is an integer or not?
UPDATE:
Thanks #chuck, #omz, and #Nikita Leonov for your help. What I posted here originally was just a simplification of the problem I was having and wrote it here without running it first. That code once updated to remove the errors (see below) runs fine actually.
The mistake I made in my real code that I was having trouble with was equally silly--I was passing the array back in to "doWhatever" instead of the item at array's index, which is why I was having problems.
Thanks for trying to help, however misguided my question was.
Sorry for wasting everybody's time!
Corrected code that runs as desired:
- (NSNumber *)doWhatever:(id)arg
{
// this NOW WORKS
if ([arg isKindOfClass:[NSNumber class]]) {
return arg;
}
else {
// what was it???
NSLog(#"arg klass=%#", [arg class]); // prints NSCFNumber
}
// This check works correctly.
if ([arg isKindOfClass:[NSArray class]]) {
for (id x in arg) {
NSNumber *result = [self doWhatever:x];
if (result) {
return result;
}
}
}
return nil;
}
- (void)someMethod
{
NSArray *myArray = [NSArray arrayWithObjects:
[NSNumber numberWithInt:1],
[NSNumber numberWithInt:2],
[NSNumber numberWithInt:3],
[NSNumber numberWithInt:4],
nil];
NSNumber *myNum = [self doWhatever:myArray];
NSLog(#"myNum=%#", myNum);
}
NSCFNumber is a subclass of NSNumber. As long as you're using isKindOfClass: rather than isMemberOfClass: or [arg class] == [NSNumber class], it should work. If not, your problem is elsewhere.

Memory errors when trying to create and populate a NSMutableDictionary

I am not a Cocoa developer, but I have been dabbling in it to build some plugins for PhoneGap. This particular plugin method is either 1) crashing the app without saying why or 2) complaining about how I release/don't release an object. I have tried a ton of things on my end, including using an Enumerator instead of the for loop. If anyone can point me in the right direction, that would be awesome. I don't mind legwork:
- (void)getPreferences:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options {
NSUInteger argc = [arguments count];
NSString* jsCallback = nil;
if (argc > 0) {
jsCallback = [arguments objectAtIndex:0];
} else {
NSLog(#"Preferences.getPreferences: Missing 1st parameter.");
return;
}
NSDictionary *defaults = [[NSUserDefaults standardUserDefaults] dictionaryRepresentation];
NSMutableArray *keys = (NSMutableArray *) [options objectForKey:#"keys"];
NSMutableDictionary *values = [[NSMutableDictionary alloc] init];
NSUInteger ky = [keys count];
for (int i = 0; i < ky; i ++) {
#try {
[values setObject:[defaults objectForKey:[keys objectAtIndex:i]] forKey:[keys objectAtIndex:i]];
}
#catch (NSException * err) {
NSLog(#"Error %#", err);
}
}
[keys release];
NSString* jsString = [[NSString alloc] initWithFormat:#"%#(%#);", jsCallback, [values JSONRepresentation]];
[defaults release];
[values release];
[webView stringByEvaluatingJavaScriptFromString:jsString];
[jsString release];
}
Human version:
options contains a dictionary with a single key of "keys"
that key contains an array of strings (that are going to be used as keys for lookup)
I want to loop through that array and
For every value that exists in defaults for that key, copy it to values using the same key
Finally, I want to send that values back as JSON (This part was working when I just passed the entire defaults object in, so I think the JSON method is working)
From your code, it follows that you 'own' objects values and jsString (the ones you created with alloc), so you should release them and not any other.
You can read more on memory management here.
Is this the whole code? Also, what exactly error do you get?
Nikita is right, it looks as though you're overreleasing defaults, which would cause a crash later when the autorelease pool gets released. Also, if I understand what you're trying to do correctly, you could create the values dictionary with a single line of code:
NSDictionary *values = [defaultsDict dictionaryWithValuesForKeys:keys];