Thursday, September 15, 2011

Validating a view in iOS / Objective C

After having won a couple of epic battles to a) set the default keyboard type to numeric and b) set the value on UIPickerView from a stored value in prefs, I'm hopeful to actually get at what I've been shooting for the last couple of days - validate all the entered data together using a routine invoked by the pressing a "save" button.

We've already set up the validation method and tested it to ensure that the start number is greater than zero. Now, we want to get a little bit more sophisticated, and make sure that the start and end numbers together coordinate. E.g., the start must be .lt. the end number. And the end number can be no greater than the amount of words in the db. We'll start out with that.

The first is easy enough:



if (startNumInt >= endNumInt) {
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Invalid Start or End Quiz Number"
message:@"Start Quiz Number must be less then or equal to End Quiz Number"
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}


For the second, we'll need to know how many rows/words exist in the DB for the vocabulary level we're at.

Let's look at our MyDatabaseReader class. It has this method, "selectAll":

-(void) selectAll
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{

int cntr_question = 0;

NSLog(@"select all, open ok");


NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words;"];

const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(jlptDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
NSLog(@"select all, prepare ok");

while (sqlite3_step(statement) == SQLITE_ROW)
{
cntr_question++;



NSString *kanji = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];

NSString *hiragana = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];

NSString *english = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 2)];

// NSLog(@"kanji: %@, hiragana: %@, english: %@", kanji, hiragana, english);

// allocate the question
question = [[Question alloc] init];

question.questionTxt = kanji;

question.number = cntr_question;

question.hiragana = hiragana;

if ([question.questionTxt length] == 0){
question.questionTxt = hiragana;
}

question.english = english;

// add the correct answer after converting it to an int
question.correctAnswer = 1;

[kanji release];
[hiragana release];
[english release];

// add it to the app state
[appState addQuestion: question];


[question release];
}
sqlite3_finalize(statement);
}
else {
NSLog(@"select all, prepare unsuccessful");
}
sqlite3_close(jlptDB);
}
else {
NSLog(@"select all, open failed");

}
}


We need to add a parameter to this to get it to read only from the current level. Here's the select statement:

NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words;"];


Ok, let's extract the method to be used by both selectAll and selectByLevel:


- (void) common_select: (NSString *) querySQL statement: (sqlite3_stmt *) statement cntr_question: (int) cntr_question {
const char *query_stmt = [querySQL UTF8String];

if (sqlite3_prepare_v2(jlptDB, query_stmt, -1, &statement, NULL) == SQLITE_OK)
{
NSLog(@"common select, prepare ok");

while (sqlite3_step(statement) == SQLITE_ROW)
{
cntr_question++;



NSString *kanji = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 0)];

NSString *hiragana = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 1)];

NSString *english = [[NSString alloc] initWithUTF8String:(const char *) sqlite3_column_text(statement, 2)];

// NSLog(@"kanji: %@, hiragana: %@, english: %@", kanji, hiragana, english);

// allocate the question
question = [[Question alloc] init];

question.questionTxt = kanji;

question.number = cntr_question;

question.hiragana = hiragana;

if ([question.questionTxt length] == 0){
question.questionTxt = hiragana;
}

question.english = english;

// add the correct answer after converting it to an int
question.correctAnswer = 1;

[kanji release];
[hiragana release];
[english release];

// add it to the app state
[appState addQuestion: question];


[question release];
}
sqlite3_finalize(statement);
}
else {
NSLog(@"select all, prepare unsuccessful");
}

}



And here's the select:

-(void) selectAll
{
const char *dbpath = [databasePath UTF8String];
sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{

int cntr_question = 0;

NSLog(@"select all, open ok");


NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words;"];

[self common_select: querySQL statement: statement cntr_question: cntr_question];

sqlite3_close(jlptDB);
}
else {
NSLog(@"select all, open failed");

}
}





And here's selectByLevel:


Well, before I do that, we really need to create a Utils method to get the current level from prefs, don't we?

I can use a "class method"/static method for this, which starts with a "+", not the "-" which means instance method (the minus is instance, plus is class).

Here's the method



+ (NSInteger) getJlptLevel
{

// retrieve the stored jlpt level from prefs

NSUserDefaults *prefs;
NSInteger jlptLevel;


prefs = [NSUserDefaults standardUserDefaults];

jlptLevel = [prefs integerForKey:@"jlptLevel"];


NSLog(@"jlptLevel from prefs: %d ", jlptLevel );

return jlptLevel;
}



Actually, let's go back to our app delegate to test all this:

NSInteger jlptLevel = [Utils getJlptLevel];
NSLog(@"Jlpt Level: %d", jlptLevel);

// not change yet
[myDatabaseReader selectAll];

Ok, good - that worked:


2011-09-15 20:29:30.172 JlptQuizApp[8710:207] jlptLevel from prefs: 3


Now, let's implement that selectByLevel.

It looks something like this:



-(void) selectByLevel:(NSInteger) level
{
const char *dbpath = [databasePath UTF8String];

sqlite3_stmt *statement;

if (sqlite3_open(dbpath, &jlptDB) == SQLITE_OK)
{
int cntr_question = 0;

NSLog(@"selectByLevel, open ok");


NSString *querySQL = [NSString stringWithFormat: @"SELECT kanji, hiragana, english FROM all_words WHERE level level = @d;", level];

[self common_select: querySQL statement: statement cntr_question: cntr_question];

sqlite3_close(jlptDB);
}
else {
NSLog(@"select by level, open failed");

}
}


And call it from the app delegate:


[myDatabaseReader selectByLevel:jlptLevel];


This is actually something I've been meaning to get to for a while - let's test it out.

Doh - stack trace. I knew this was going too smoothly!

It's crashing on this statement in AppState:



NSNumber *key = [shuffledQuestionDictKeyList objectAtIndex:currentShuffledArrayIndex];




This is set up in the app delegate with this code:



[myDatabaseReader selectByLevel:jlptLevel];

// [myDatabaseReader selectAll];

self.appState = myDatabaseReader.appState;

[myDatabaseReader release];


appState.currentShuffledArrayIndex = 1;

NSArray *keys = [appState.questionDict allKeys];
appState.shuffledQuestionDictKeyList = [keys shuffledArray];



There are a couple of places where this could be going wrong - the select by level, or the subsequent logic to set up the keys array. My money's on the first, since that's the new kid on the block. Let's use the debugger!

Yes, the keys array has 0 objects.

Right, stepping into it and checking the SQL statement shows that the level was coming out as "@d" instead of the number.

So change this:

@"SELECT kanji, hiragana, english FROM all_words WHERE level level = @d;"

to this:

@"SELECT kanji, hiragana, english FROM all_words WHERE level level = %d;"

Ah, try again. I have one extra level in there.

@"SELECT kanji, hiragana, english FROM all_words WHERE level = %d;"

That's the ticket. Now it's working fine. That was key, I was going to get to that next.

Ok, now let's go ahead and get back to our original intent - finding out how many rows are in the db at that level. We could check the app state question dictionary, and add 1 to it.


It looks like I don't have it in SettingController yet.

Ok, I went through the process of setting up a property for it on the startController and the SettingsController, then setting it from the app delegate to the startController, and the from there onto the settings controller. What I could've done is this, as I did in the question controller:



QuizAppDelegate *delegate =
(QuizAppDelegate *)[[UIApplication sharedApplication] delegate];

appState = delegate.appState;



But, anyway, let's print it out:


NSLog(@"SettingsController, appState.questionDict count: %d", [appState.questionDict count]);


Here we go:

2011-09-15 21:21:51.738 JlptQuizApp[9107:207] SettingsController, appState.questionDict count: 1835


Ok, to finish things off:




if (endNumInt > [appState.questionDict count]) {

// stringWithFormat autoreleases
NSString *myMessage = [NSString stringWithFormat:@"End Quiz Number must be less then or equal to %d",[appState.questionDict count] + 1];

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"InvalidEnd Quiz Number"
message:myMessage
delegate:nil
cancelButtonTitle:@"OK"
otherButtonTitles:nil];
[alert show];
[alert release];
}



And that's a wrap :) Image below:


No comments:

Post a Comment