Well, now that we've cut down some of the low-hanging fruit, it's time to get into the meat of the app. What we did yesterday was wire up the new database to the our prototype app. The first thing we must address to make it "real" is to substitute the dummy choices with random, real choices taken from the existing list of Japanese words. We have a fairly sophisticated algorithm which makes the multiple choice answers look more more like the actual answer by searching for words which have similar endings. But, for our current purposes, we'll choose a simpler, purely random algorithm, just go keep things moving without getting bogged down.
So, how can we do this? One possibility is to just choose an id number at random, and just grab it from the database using a direct read. But, we've already read the entire database into a data structure and it's possible we'll leverage that when we implement our algorithm later on. So, what is this data structure?
Here's where it adds the question, in the DatabaseReader code:
// add it to the app state
[appState addQuestion: question];
Let's take a look at that method:
- (void)addQuestion:(Question *)question {
// Convert int to NS Number
NSNumber *number = [NSNumber numberWithInt:question.number];
// Add number to dictionary of questions
[questionHash setObject: question
forKey: number];
}
So, this question hash looks like a hash table which is keyed by question number. Here it is:
// this will contain the hash table of question objects
NSMutableDictionary *questionHash;
Ok, looks good. So, for now, all we really need to do is determine the size of the hash table, than grab a random number that exists somewhere inside that and get that. Then, just get another random number between 1 and 4 (or 0 and 3) and then replace that with the true answer. When they select that, we'll compare the two strings, and if it's the same, it's right - otherwise - off with their heads!
Ok, let's get this parade moving. First, how to get the size of NSmutable dictionary? Well, where is this code going run, first of all?
Ok, this looks like a pretty good candidate, in the QuestionController:
// Implement viewDidLoad to do additional setup after loading the view, typically from a nib.
- (void)viewDidLoad {
QuizAppDelegate *delegate =
(QuizAppDelegate *)[[UIApplication sharedApplication] delegate];
// get answers
appState = delegate.appState;
currentQuestion = appState.currentQuestion;
answers = [currentQuestion answerArray];
// change this to a lable above the table
self.title = @"Jlpt5QuizApp";
tableView.rowHeight = 200;
[super viewDidLoad];
}
So, it's looks like it's taking the answer for the currentQuestion's answerArray method. What does that look like:
/* return answers in a sorted array */
- (NSMutableArray *) answerArray {
// create arry
NSMutableArray *answers = [[NSMutableArray alloc] init];
for (int i = 1; i <= [answerHash count]; i++) {
NSNumber *key = [NSNumber numberWithInt:i];
NSString *answer = [answerHash objectForKey:key];
/* NSLog(@"answer is: %@", answer); */
[answers addObject:answer];
}
return answers;
}
Ok, it's an NSMutableArray. Hmm - it's expecting to have the answer number assigned as well. No harm done - it's irrelevant in this case, since we're randomly choosing the answers. We'll just assign it in an incremental way.
Ok, so, there's a little problem in that it's coming from Question, which already has the answers preloaded and doesn't have or necessarily need access to the appstate, which holds the whole Question array from which would could choose.
So, the question is, do we break a little bit of encapsulation so the question can do a lazy-load of the answer, or do we load all the answers up in question activity? The question controller is crowded enough, so let's put it into the question method. That means we'll need to give it access to AppState, which everyone and his brother seems to have access to already. Actually, it seems to be passed from class to class, but the home appear to be in the app delegate, which works here. We could actually have the QuestionController pass it as a parameter to the method, which means that the Question wouldn't have to have it as a property. That's a little bit cleaner, at least.
Actually, this piece of code is already doing the job for use:
QuizAppDelegate *delegate =
(QuizAppDelegate *)[[UIApplication sharedApplication] delegate];
// get answers
appState = delegate.appState;
currentQuestion = appState.currentQuestion;
answers = [currentQuestion answerArray];
Ok, so, the way of passing parameters in iOS is somehow funky, but I mean c'mon, it's just passing a parameter. I think it uses a ":" to distinguish it - let's look for an example.
Here are a couple of declarations:
-(id)initWithIndexPath:(NSIndexPath *)indexPath;
-(IBAction)popViewController:(id)sender;
Because this is a C-based language, we have the forbidding differences in how we declare parameters, either as pointers, which would be pass-by-value, or just a strait declaration. I'm guessing we declare it as a pointer, something like this:
- (NSMutableArray *) answerArray:(AppsState*) appState;
But how do we call it? if it's a pointer, do we need to call it with the &? That's supposed to pull the actual address. We don't prefix it with a "*", because that woud extract the contents and that's not what we want with a pointer. What if we called it with neither? That's something I don't undersand right now.
There's a good example in SO:
http://stackoverflow.com/questions/722651/how-do-i-pass-multiple-parameters-in-objective-c
which also clears up some confusion I still had about method names.
The basic thing to remember about method calls is that:
Anything to the left of a colon ":" is the name of the parameter. Think of it as a helpful description, which supplement the variable name.
You need to have a ":" for each parameter.
The name, or call it "positional" name as opposed to "variable" name, is optional, but apparently recommended.
The "positional name" is actually par of the method name.
The problem question is, is the first "positional name" supposed to be a description of the first parameter, or is it supposed to be an overall description of the method?
And example of a call is given:
NSString *obj = @"Hello, World!";
int index = 5;
[array insertObject:obj atIndex:index];
Where the declaration is:
- (void)insertObject:(id)anObject atIndex:(NSUInteger)index;
But that doesn't really clarify how we should invoke the method in our case. Is there a difference between our app state and the NSString, which has the "id" type, which I'm not sure about - was it supposed to kind of a catch-all?
I remember from the Ritchie book the declaring something as a pointer and using the same * to get the contents of where it was pointing to makes it intuitive. But in the method calls, I've never quite nailed that one down for some reason. I would think if you call a method which is expecting a pointer you should give it that pointer - i.e. use the & method.
Well, google shows that we don't use the &, although I still wish I knew the reason. Anyway, here's our punched up answerArray method:
- (NSMutableArray *) getAnswerArray : (AppState *) appState
{
NSDictionary *questionHash = [appState questionHash];
NSArray *tempArray = [questionHash allKeys];
int size = [tempArray count];
// create array
NSMutableArray *answers = [[NSMutableArray alloc] init];
for (int i = 0; i <= 4 ; i++) {
// for (int i = 1; i <= [answerHash count]; i++) {
// Get random value between 0 and number of questions
int rand = arc4random() % (size - 1); // starts at zero, not 1
rand++; // between 1 and size
NSNumber *key = [NSNumber numberWithInt:rand];
Question *question = [questionHash objectForKey:key];
NSString *answer = [question questionTxt];
// NSString *answer = [answerHash objectForKey:key];
/* NSLog(@"answer is: %@", answer); */
[answers addObject:answer];
}
return answers;
}
Ok, so this will just randomly grab a different word and plug it into answers. Later, we'll have a "hiragana" field which has the real answer, but this is going to do the trick for now.
Ok. Let's go back to the calling class. Here's the call:
answers = [currentQuestion getAnswerArray: appState];
Let's try it!
Note watch out for mutually dependent imports. If A imports by and vice versa, you get a totally useless syntax error message. The way out of the loop is to use the forward @class declaration. So, when the compiler of A sees that, it doesn't try to figure it out. Later on when it's compiling the implementation it sees it, but somehow that doesn't bollox things up.
Anyway, once I got that straightened out by consulting SO, we're back in business. However, the we're showing 5 answers when we should be only showing 4. Let's try this:
for (int i = 0; i < 4 ; i++) {
Ok, it's not showing five anymore - but occasionally it's finding blanks. That's because some of the hiragana entries are blank. That's normal, and we have already figured out a way around that. For now, here's a picture of the (soon-to-change) interface with the kanji being displayed for answers:
No comments:
Post a Comment