I am writing an app, and one of the features I need to implement requires the app to pull JSON data from a website, store it in a dictionary, then be able to use all of the keys and display the values. I will not know what the structure of the dictionary will look like, so I was hoping to recursively traverse the dictionary to retrieve all of the information.
I have the the JSON stored in a dictionary from the website that I need, and when I put the dictionary variable in a println() statement it displays correctly.
I found this link and I think this, or some variation of this should work, but I am still fairly new to swift and I am not sure how this translates from Objective-c to swift.
The part of that link that I am interested in is this:
(void)enumerateJSONToFindKeys:(id)object forKeyNamed:(NSString *)keyName
{
if ([object isKindOfClass:[NSDictionary class]])
{
// If it's a dictionary, enumerate it and pass in each key value to check
[object enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) {
[self enumerateJSONToFindKeys:value forKeyNamed:key];
}];
}
else if ([object isKindOfClass:[NSArray class]])
{
// If it's an array, pass in the objects of the array to check
[object enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
[self enumerateJSONToFindKeys:obj forKeyNamed:nil];
}];
}
else
{
// If we got here (i.e. it's not a dictionary or array) so its a key/value that we needed
NSLog(#"We found key %# with value %#", keyName, object);
}
}
I'm not sure how to go about this, any help or pointers in the right direction are appreciate. Thanks!
EDIT: This is the direction I started to go in, but there were a lot of errors. I tried to fix them but didn't have much luck.
func enumerateJSONToFindKeys(id:AnyObject, keyName:NSString){
if id.isKindOfClass(NSDictionary)
{
AnyObject.enumerateKeysAndObjectsUsingBlock(id.key, id.value, stop:Bool())
{
self.enumerateJSONToFindKeys(id.value, forKeyNamed: keyName)
}
}
else if id.isKindOfClass(NSArray)
{
}
}
Try this:
func enumerateJSONToFindKeys(object:AnyObject, forKeyNamed named:String?) {
if let dict = object as? NSDictionary {
for (key, value) in dict {
enumerateJSONToFindKeys(value, forKeyNamed: key as? String)
}
}
else if let array = object as? NSArray {
for value in array {
enumerateJSONToFindKeys(value, forKeyNamed: nil)
}
}
else {
println("found key \(named) value \(object)")
}
}
It uses the Swift as? conditional casting operator as well as native iteration over both the NSDictionary and NSArray.
Related
Without unintentionally killing performance, does this appear at first glance to be acceptable for perhaps 200 guid strings in one list compared for equality with 100 guid strings from another list to find the matching indexes.
I have a method signature defined like so...
-(NSArray*)getItemsWithGuids:(NSArray*)guids
And I wanted to take that passed in array of guids and use it in conjunction with this array...
NSArray *allPossibleItems; // Has objects with a property named guid.
... to obtain the indexes of the items in allPossibleItems which have the matching guids from guids
My first instinct was to try indexesOfObjectsPassingTest but after putting together the block, I wondered whether the iOS framework already offers something for doing this type of compare more efficiently.
-(NSArray*)getItemsWithGuids:(NSArray*)guids
{
NSIndexSet *guidIndexes = [allPossibleItems indexesOfObjectsPassingTest:^BOOL(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop)
{
SomeObjWithGuidProperty *someObject = obj;
for (NSString *guid in guids) {
if ([someObject.guid isEqualToString:guid]) {
return YES;
}
}
return NO;
}];
if (guidIndexes) {
// Have more fun here.
}
}
Since you're working with Objective-C (not Swift) check out YoloKit. In your case, you can do something like:
guids.find(^(NSString *guid){
return [someObject.guid isEqualToString:guid];
});
My thought would be to use a set -
-(NSArray*)getItemsWithGuids:(NSArray*)guids inAllObjects:(NSArray *)allObjects
{
NSSet *matchGuids=[NSSet setWithArray:guids];
NSMutableArray *matchingObjects=[NSMutableArray new];
for (SOmeObjectWithGuidProperty *someObject in allObjects) {
if ([matchGuids contains:someObject.guid]) {
[matchingObjects addObject:someObject];
}
}
return [matchingObjects copy];
}
Your code looks like it would have O(n^2) performance, which is bad. I think the solution of converting guids to an NSSet and then using NSSet's containsObject would likely be much more performant. You could rewrite your indexesOfObjectsPassingTest code to use an NSSet and containsObject pretty easily.
If order doesn't matter much, I would suggest to change data structure here. Instead of using NSArray, consider to use NSDictionary with guid as key and someObject as value. In this case, you should use -[NSDictionary objectsForKeys:notFoundMarker:] method to obtain objects.
It will work much faster, than enumeration trough 2 arrays. If the NSDictionary key have a good hash function, accessing an element, setting an element, and removing an element all take constant time. NSString has good hash.
-(NSArray*)getItemsWithGuids:(NSArray*)guids {
NSArray *objectsAndNulls = [allPossibleItemsDictionary objectsForKeys:guids notFoundMarker:[NSNull null]];
if (objectsAndNulls) {
// Have more fun here.
// You should check that object in objectsAndNulls is not NSNull before using it
}
return objectsAndNulls;
}
UPD Unfortunately, there is no way to pass nil as notFoundMarker. If you can't provide usable notFoundMarker value and don't want to perform additional checks, you can query objects one by one and fill NSMutableArray. In this case you will avoid pass trough array to remove NSNulls:
-(NSArray*)getItemsWithGuids:(NSArray*)guids {
NSMutableArray *objects = [NSMutableArray arrayWithCapacity:guids.count];
for (NSString *guid in guids) {
SomeObjWithGuidProperty *object = allPossibleItemsDictionary[guid];
if (nil != object) {
[objects addObject:object];
}
}
if (nil != objects) {
// Have more fun here.
}
return object;
}
A JSON API feed used by our iOS ObjectiveC app is a bit flaky, so sometimes a field is null.
When parsing JSON we use
NSDictionary *json = [self JSONFromResponseObject:responseObject];
Then try to use the fields with e.g.
[widgetIDArray addObject:widget[#"name"][#"id"]];
Where sometimes the "name" field will be a null. Do we:
1) Ask the API provider to clean up their flaky API code
2) Check for null each and every time we try to use something from the json dict
if ( ![widget[#"name"] isKindOfClass:[NSNull class]] )
3) Use try - catch
#try {
[widgetIDArray addObject:widget[#"name"][#"id"]];
}
#catch (NSException *exception)
{
NSLog(#"Exception %#",exception);
}
ANSWER:
Thanks for the answers, below. Here is the extension to NSObject I added that allows me to get deeply nested JSON items that may or may not be present.
First call with something like
self.item_logo = [self valueFromJSONWithKeyArray:event withKeyArray:#[#"categories",#"bikes",#"wheels",#"model",#"badge_uri"]];
Here is the code in NSObject+extensions.m
- (id) valueFromJSONWithKeyArray:(id)json withKeyArray:(NSArray *)keyArray
{
for (NSString * keyString in keyArray)
{
if ([json[keyString] isKindOfClass:[NSObject class]])
{
json = json[keyString]; // go down a level
}
else
{
return nil; // we didn't find this key
}
}
return json; // We successfully found all the keys, return the object
}
null in a JSON response isn't "flaky", it is absolutely standard.
Even if it was "flaky", any message that you receive from the outside is an attack vector that could allow an attacker to hack into your program, so resilience is required. Crashing when your receive a null allows a DOS attack against your application.
#try / #catch is awful. Exceptions are thrown in response to programming errors. You don't catch them, you fix your code.
How do you fix your code? Simple. Write a few helper methods in an NSDictionary extension.
First you don't know that json is a dictionary. So you add an NSDictionary class method where you pass in anything and it returns what you passed if it is a dictionary and nil (with appropriate logging) if it is anything else.
Next you assume that there is a dictionary under the key "name". So you write an extension "jsonDictionaryForKey" which returns a dictionary if there is one, and nil (with appropriate logging) if it is anything else.
And so on. Make your JSON parsing bullet proof if you want to call yourself a professional developer. For extra bonus points you add a method which will take a dictionary and list all keys that are present that you didn't ask for - so you know if your API is sending things that you don't expect.
You can delete all NSNULL values in your JSON object. Here is a function I used in my library to git rid of all null values in a JSON object.
id BWJSONObjectByRemovingKeysWithNullValues(id json, NSJSONReadingOptions options) {
if ([json isKindOfClass:[NSArray class]]) {
NSMutableArray *mutableArray = [NSMutableArray arrayWithCapacity:[(NSArray *)json count]];
for (id value in (NSArray *)json) {
[mutableArray addObject:BWJSONObjectByRemovingKeysWithNullValues(value, options)];
}
return (options & NSJSONReadingMutableContainers) ? mutableArray : [NSArray arrayWithArray:mutableArray];
} else if ([json isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *mutableDictionary = [NSMutableDictionary dictionaryWithDictionary:json];
for (id<NSCopying> key in [(NSDictionary *)json allKeys]) {
id value = [(NSDictionary *)json objectForKey:key];
if (isNullValue(value)) {
[mutableDictionary removeObjectForKey:key];
} else if ([value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSDictionary class]]) {
[mutableDictionary setObject:BWJSONObjectByRemovingKeysWithNullValues(value, options) forKey:key];
}
}
return (options & NSJSONReadingMutableContainers) ? mutableDictionary : [NSDictionary dictionaryWithDictionary:mutableDictionary];
}
return json;
}
After all null values have been cleared, perhaps the exceptions will gone too.
An approach that's often used is categories similar to these on NSDictionary and NSMutableDictionary, that ignore nil and NSNull.
#interface NSDictionary (nullnilsafe)
- (ObjectType)nullnilsafe_objectForKey:(KeyType)aKey;
#end
#implementation NSDictionary
- (ObjectType)nullnilsafe_objectForKey:(KeyType)aKey
{
id obj = [self objectForKey:aKey];
if (obj && ![obj isKindOfClass:[NSNull class]] ){
return obj;
}
return nil;
}
#end
#interface NSMutalbeDictionary (nullnilsafe)
- (void)nullnilsafe_setObject:(ObjectType)anObject forKey:(id<NSCopying>)aKey;
#end
#implementation NSMutalbeDictionary
- (void)nullnilsafe_setObject:(ObjectType)anObject forKey:(id<NSCopying>)aKey
{
if (anObject && aKey && ![anObject isKindOfClass:[NSNull class]]){
[self setObject:anObject forKey:aKey];
}
}
#end
I receive data for an object person in sets of 5. Let's say name,age,gender,email,number. I did following to add the strings to NSobject:
DataObject *data=[DataObject new];
data.name=#"name";
data.age=#"age";
data.email=#"email";
//here i want to check for duplicates
[personArray addObject:data];
However, I want to check if the personArray is having the duplicate NSObjects or not.
I tried this,but it didnt work:
if(![personArray containsObject:data]){
//add data
}
Edit: Actually, this is what I am trying to do:
I am getting the JSON repsonse and I am adding the properties into array. Before I used to get only one property,in that case, I did the following to eliminate the duplicates:
[JSON[#"person"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj[#"email"] isEqual:[NSNull null]] && ![personArray containsObject:obj[#"email"]] ) {
[personArray addObject:obj[#"email"]];
}
}];
Later I got 5 properties for person, so I thought instead of adding them all to the array, I used NSObject class to tie the properties together and add one person to the array.
[JSON[#"person"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (![obj[#"email"] isEqual:[NSNull null]] && ![personArray containsObject:obj[#"email"]] ) { //how to check for the duplicates here?
DataObject *data=[DataObject new];
data.name=#"name";
data.age=#"age";
data.email=#"email";
[personArray addObject:data];
}
}];
If you do this:
DataObject *data = [DataObject new];
You have just created a new instance of data. No other object inside the personArray can be equal to that new instance.
I assume you're actually trying to check to see if there is a data object that contains the same properties as other data objects in the personArray. There's a number of ways you could do this (I like Zaph's answer, it's clean), but for simplicity...
DataObject *data=[DataObject new];
data.name=#"name";
data.age=#"age";
data.email=#"email";
BOOL contains = NO;
for (DataObject *object in personArray) {
if ([object.name isEqualToString:data.name] && [object.age isEqualToString:data.age] && [object.email isEqualToString:data.email]) {
contains = YES;
break;
}
}
if (!contains) {
[personArray addObject:data];
}
You need to implements isEqual for the DataObject class. Then [personArray containsObject:data] should work.
For details see:
Equality by Mattt Thompson.
Implementing Equality and Hashing by Mike Ash
I have an NSArray that I'm using in my iOS application which is holding data of three types:
NSDate, NSString, and NSNumber
What I would like to do is iterate this NSArray in a for loop to check to see if the objects are null, however, I'm unsure how to do this because the array contains objects of different types instead of one single type. This is what I am thinking of doing:
for (id widget in myArray)
{
if ([widget isKindOfClass:[NSDate class])
{
if (widget == nil) {
widget = #"";
}
}
else if ([widget isKindOfClass:[NSString class])
{
if (widget == nil) {
widget = #"";
}
}
else if ([widget isKindOfClass:[NSNumber class])
{
if (widget == nil) {
widget = #"";
}
}
}
However, I am getting the compilation error: "Fast enumeration variables can't be modified by ARC by default; declare the variable __strong to allow this." I am not sure ahead of time what type the object is going before the iteration, so how do I get around this?
NSArray can't hold nil values. Just check for NSNull
for (id widget in myArray)
{
if ([widget isKindOfClass:[NSNull class]])
//do what you need
}
What I am trying to achieve is to search an array for a string, here is the code for searching the array
if ([EssentialsArray containsObject:imageFilePath]) {
NSLog(#"YES");
}
else {
NSLog(#"NO");
NSLog(#"%#", ImageFilePath);
NSLog(#"%#", EssentialsArray);
}
The NSLogs return this for the imageFilePath:
/var/mobile/Applications/5051DC84-CAC8-4C1D-841D-5539A1E28CB1/Documents/12242012_11025125_image.jpg
And this for the EssentialsArray:
(
{
EssentialImage = "/var/mobile/Applications/5051DC84-CAC8-4C1D-841D-5539A1E28CB1/Documents/12242012_11025125_image.jpg";
}
)
And then obviously they return a "NO" value because the array couldn't find the string.
Thanks in advance
The issue is that the invocation of [EssentialsArray containsObject:imageFilePath] clearly assumes that EssentialsArray is an array of strings, whereas it's not. It's an array of dictionary entries with one key, EssentialImage.
There are at least two solutions. The first is to make an array of strings for those dictionary entries whose key is EssentialImage:
NSArray *essentialImages = [essentialsArray valueForKey:#"EssentialImage"];
if ([essentialImages containsObject:imagePath])
NSLog(#"YES");
else
NSLog(#"NO");
Depending upon the size of your essentialsArray (please note, convention dictates that variables always start with a lower case letter), this seems a little wasteful to create an array just so you can do containsObject, but it works.
Second, and better in my opinion is to use fast enumeration to go through your array of dictionary entries, looking for a match. To do this, define a method:
- (BOOL)arrayOfDictionaries:(NSArray *)array
hasDictionaryWithKey:(id)key
andStringValue:(NSString *)value
{
for (NSDictionary *dictionary in array) {
if ([dictionary[key] isEqualToString:value]) {
return YES;
}
}
return NO;
}
You can now check to see if your array of dictionaries has a key called #"EssentialImage" with a string value equal to the string contained by imagePath
if ([self arrayOfDictionaries:essentialsArray
hasDictionaryWithKey:#"EssentialImage"
andStringValue:imagePath])
NSLog(#"YES");
else
NSLog(#"NO");
Update:
You can also use predicates:
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"EssentialImage contains %#", imagePath];
BOOL found = [predicate evaluateWithObject:array];