I am doing a project in which I am loading a table view with JSON data from an asynchronous connection. I am using a switch statement to load each row, like so:
dictionaryData = [responseString JSONValue];
switch (indexPath.row)
{
case 0:
{
NSString *name = [NSString stringWithFormat:#"%# : %# %#",#"Name",[dictionaryData valueForKey:#"firstName"],[dictionaryData valueForKey:#"lastName"]];
cell.textLabel.text = name;
break;
}
case 1:
{
NSString *email = [NSString stringWithFormat:#"%# : %#",#"Email",[dictionaryData valueForKey:#"email"]];
cell.textLabel.text = email; }
break;
There are 8 rows and i have to write 8 switch cases , which i think makes my method too long.
Can anyone tell me that is there any alternative to switch statements.
Replace your switch with an array. Each index in the array will correspond to one of the former cases. The array should be set up with the keys that you need
keyArray = [NSArray arrayWithObjects:#"Name", #"Email", ..., nil];
dictionaryData = [responseString JSONValue];
NSString * rowKey = [keyArray objectAtIndex:[indexPath row]];
[[cell textLabel] setText:[dictionaryData objectForKey:rowKey]];
In some cases an object would be a better alternative.
Update
I detailed how one might approach this (in an overengineered manner) here:
What alternatives to a switch statement could I use to update my UITableViewCells?
The alternative to switch is to use a series of if/else statements, which doesn't make the code any shorter. If your method is too long or too complex, move the body of each case into its own method, and then just call that method from its corresponding case.
Related
Hey guys I have this piece of code for a UISearchBar that searches dynamically. But since the DataSource is a VERY large NSArray the search process has an annoying lag that I need/must/have to get rid of.
The problem is that I could not find a way to do the search in a non-dynamically way like the user would type the search string and only after hitting the Search button on the keyboard is when the search process would being and present the result.
My code is as follows:
-(void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
if (searchText.length == 0) {
isFiltered = NO;
} else {
isFiltered = YES;
filteredCodigos = [[NSMutableArray alloc] init];
for (NSDictionary *item in values) {
NSString *strSymbol = [item objectForKey:#"symbol"];
NSString *strName = [item objectForKey:#"name"];
NSRange strRangeSymbol = [strSymbol rangeOfString:searchText options:NSCaseInsensitiveSearch];
NSRange strRangeName = [strName rangeOfString:searchText options:NSCaseInsensitiveSearch];
if ((strRangeSymbol.location != NSNotFound) || (strRangeName.location != NSNotFound)) {
[filteredCodigos addObject:item];
}
}
}
[_tableView reloadData];
}
You can use the searchBarSearchButtonClicked: search bar delegate method to trigger the search instead of searchBar:textDidChange:.
You can also, if using searchBar:textDidChange:, require that the length of the search string is at least 2 or 3 characters long before performing a search.
You should probably also consider changing your data source so, rather than using an array, you're using Core Data (or similar). Both so you don't need everything in memory at once and so you can run efficient searches.
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.
Could you please guide me on how not to use a for loop in this example? I want to execute it one operation if possible:
NSArray *statuses = [parser objectWithString:json_string error:nil];
for (NSDictionary *status in statuses) {
test = [status objectForKey:#"USER_ID"];
}
Thanks.
NSArray *statuses = [parser objectWithString:json_string error:nil];
if ([statuses count]>0) {
test=[[statuses objectAtIndex:0] objectForKey:#"USER_ID"];
}
If you know that the NSArray has only one element (or that the NSDictionary you want is always the first, or at some other fixed position), use [statuses objectAtIndex:0] (or whatever index) to fetch the status element.
Okay I've been trying at this for about 2-3 hours now and I don't seem to quite get it. Here is the code and a brief explanation:
I'm trying to make two lists of words, pull one word from each of those lists at random, and display both words (along with a third) on the screen when a button is pressed. Here is the code:
#import "Project001ViewController.h"
#implementation Project001ViewController
-(ArrayOfWords *) advs
{
if(!advs){
advs = [[ArrayOfWords alloc] init];
NSString* advpath = #"/WordLists/adverbs.txt";
NSLog(#"1");
[[self advs] populateListOfWords:advpath];
}
return advs;
}
-(ArrayOfWords *) adjs
{
if (!adjs) {
adjs = [[ArrayOfWords alloc] init];
NSString* adjpath = #"/WordLists/adjectives.txt";
[[self adjs] populateListOfWords:adjpath];
NSLog(#"2");
}
return adjs;
}
- (IBAction)generate:(UIButton *)sender;
{
//int randy = arc4random() % 11;
//NSNumber* num= [NSNumber numberWithInteger:randy];
NSString* obj = #"app";
NSString* adverb = [[self advs] randomItem];
NSString* adjective = [[self adjs] randomItem];
NSLog(#"%i %i",[adjs size],[advs size]);
NSLog(#"1 %# %# %#.",adverb, adjective, obj);
//NSLog(#"%#",thePhrase);
[display setText:#"Hi"];
}
#end
I'm having trouble on the last NSLog line:
NSString* obj = #"app";
NSString* adverb = [[self advs] randomItem];
NSString* adjective = [[self adjs] randomItem];
NSLog(#"%i %i",[adjs size],[advs size]);
NSLog(#"1 %# %# %#.",adverb, adjective, obj);
Instead of getting the two randomly selected words (using arc4random() to produce them) the array returns Null. But I know FOR CERTAIN. That the array's are not empty because the NSLog Line where I print [adjs size] and [advs size] I get the correct sizes of the list of words. I just want to know what is causing them to print Null here.
populateListOfWords, randomItem, and size methods:
- (NSArray *) populateListOfWords:(NSString *) path {
//gets the components of the file into an NSString
NSString *wordListString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//returns an array of all the words (uses the next line indicator '\n' to know when it's at the end of the word
NSArray* words = [wordListString componentsSeparatedByString:#"\n"];
length=(NSNumber*)([words count]);
return words;
}
-(NSString*) randomItem{
//returns random object in list
return (NSString*)[list objectAtIndex:(arc4random() % (int)length)] ;
}
-(int) size{
//returns size of list
return (int)length;
}
(If more code is needed let me know and thank you in advanced for any and all help).
I believe there is a problem with the paths. It is impossible to have access to the path /WordLists/adjectives.txt in iOS due to the application sandbox. I suggest you add these files to the application by dragging and dropping them onto the project. You can get the file paths for resources in application bundle using
NSString * path = [[NSBundle mainBundle] pathForResource:#"adjectives" ofType:#"txt"];
Now pass this path to the method populateListOfWords:.
Because of the incorrect path, I believe wordListString is nil and everything else follows to be that.
Another thing is that int and NSNumber are not toll free bridged like NSStrings and other foundation objects. So
length=(NSNumber*)([words count]);
is incorrect. I suggest you define length as int or better NSUInteger to match the type count method returns.
This method is the problem:
- (NSArray *) populateListOfWords:(NSString *) path {
//gets the components of the file into an NSString
NSString *wordListString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//returns an array of all the words (uses the next line indicator '\n' to know when it's at the end of the word
NSArray* words = [wordListString componentsSeparatedByString:#"\n"];
length=(NSNumber*)([words count]);
return words;
}
It wasn't actually putting the words in a list that anyone else could access. I had to just modify it like so:
- (void) populateListOfWords:(NSString *) path {
//gets the components of the file into an NSString
NSString *wordListString = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
//returns an array of all the words (uses the next line indicator '\n' to know when it's at the end of the word
NSArray* words = [wordListString componentsSeparatedByString:#"\n"];
list = words;
length=(int)([words count]);
}
Now it gives me the correct output. But for some reason when I press the button twice it crashes. Oh well that's a new problem. Thanks again for all the help.
UPDATE
Turns out advs and adjs were being released so the second go around it was trying to access a nil value because when I call [self advs] [self adjs] the pointers exist, but their contents do not. I had to go back and refill them each time basically removing the if (!advs) and if (adjs) parts. It now works as intended.
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];