Showing posts with label IOS; navController. Show all posts
Showing posts with label IOS; navController. Show all posts

Monday, August 29, 2011

Figuring out the next step

Ok, so, we're attempting to salvage something out of today after the nightmarish white table view issue. Really need to figure that one out. But, we've redone our changes and are back to where we started earlier today. That is, we now have an initial display which calls the second "quiz" display. That second quiz display is kind of a mockup at this point - it's displaying random data that doesn't have a whole lot to do with the problem.

All right. Well, the first order of business really has got to be figuring how the data is getting loaded into the table view.

This method might give an indication:


- (void) viewWillAppear:(BOOL)animated{

// NSLog(@"view will appear was called");

// won't be animated on the first call
if (animated) {
appState.currentQuestionNumber++;
[appState saveState];
}

currentQuestion = appState.currentQuestion;

answers = [currentQuestion answerArray];

// triggers reload, see veiwDidLoad method
[tableView reloadData];
}


So, the question and answers will somehow be pulled in into the question display at the top and the answers. Let's keep going.

// 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 = @"JlptQuizApp";

tableView.rowHeight = 200;

[super viewDidLoad];
}


I'm not sure why I loaded it twice. Either it's a mistake, or one of them is a one-off.

This one looks interesting:

- (UITableViewCell *)tableView:(UITableView *)tv
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{

// get the table veiw cell. Alwasy use the dequeue method
UITableViewCell *cell =
[ tv dequeueReusableCellWithIdentifier:@"cell"];


if( nil == cell ) {

// this sets up a resusable table cell
cell = [ [[UITableViewCell alloc]
initWithFrame:CGRectZero reuseIdentifier:@"cell"] autorelease];

// support line break mode for multiline
cell.textLabel.lineBreakMode = UILineBreakModeWordWrap;

// 0 means any number of lines - necessary for multiline
cell.textLabel.numberOfLines = 0;

// set it up with a consistent font with the height calculation (see below)
cell.textLabel.font = [UIFont fontWithName:@"Helvetica" size:30.0];
}


// set the answer

if (indexPath.row < answers.count ) {

NSString *answer = [answers objectAtIndex:indexPath.row];
cell.textLabel.text = answer;
}

return cell;

}


So, if I were to make a rough estimate, it looks like it's using the index of the row to access the corresponding row in the array, and then sets it on the table display.

How about the question?


Ah - here it is:

- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section {

// Create label with section title
headerLabel = [[[UILabel alloc] init] autorelease];
headerLabel.textAlignment = UITextAlignmentCenter;
headerLabel.frame = CGRectMake(20, 6, 300, 30);
headerLabel.backgroundColor = [UIColor clearColor];
headerLabel.textColor = [UIColor whiteColor];
headerLabel.font = [UIFont fontWithName:@"Helvetica-BoldOblique" size:45.0];
// support line break mode for multiline
headerLabel.lineBreakMode = UILineBreakModeWordWrap;

// 0 means any number of lines - necessary for multiline
headerLabel.numberOfLines = 0;
headerLabel.text = currentQuestion.questionTxt; <==== set here

// very important!
[headerLabel sizeToFit];

// Create header view and add label as a subview
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, SectionHeaderHeight)];
view.backgroundColor = [UIColor grayColor];
[view autorelease];
[view addSubview:headerLabel];


return view;
}



So, there's nothing to mess with in the interface here. It's just setting up the questions and answers.

So, how do they get set up? Coming in from the other side, we know we have an XMLRead class. It parses the XML and creates some kind of Array of Question object, which probably contains a question and an array of answers.

Here's some of the code:


Here it sets up the tags and the AppState object:

- (id)init
{
// Call the superclass's designated initializer
self = [super init];

// Did the superclass's initialization fail?
if (!self)
return nil;

appState = [[[AppState alloc] init] autorelease];

/* XML Tag Names */
XML_TAG_QUESTION_BLOCK = @"questions";

XML_TAG_QUESTION = @"question";

XML_TAG_QUESTION_ATTRIBUTE_NUMBER = @"number";
XML_TAG_QUESTION_ATTRIBUTE_TEXT = @"text";
XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER = @"correct_answer";

XML_TAG_ANSWERS = @"answers";
XML_TAG_ANSWER = @"answer";
XML_TAG_ANSWER_ATTRIBUTE_TEXT = @"text";

// Return the address of the newly initialized object
return self;
}



The key in the read is this method.

- (void)parser:(NSXMLParser *)parser
didStartElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName
attributes:(NSDictionary *)attributeDict
{

if ([elementName isEqualToString:XML_TAG_QUESTION]){

// answer counter - initialize for new question
cntr = 1;

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

// get the question text
question.questionTxt = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_TEXT]; // XML_TAG_QUESTION_ATTRIBUTE_TEXT

// get get the question number
NSString *text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_NUMBER];

// convert from the text value
question.number = [text intValue];

// Now get the question answer
text = [attributeDict valueForKey:XML_TAG_QUESTION_ATTRIBUTE_CORRECT_ANSWER];

// add the correct answer after converting it to an int
question.correctAnswer = [text intValue];

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

[question release];

}

if ([elementName isEqualToString:XML_TAG_ANSWER]){

// convert counter to an object for storage in dictionary
NSNumber *counter = [NSNumber numberWithInt:cntr];

// Example of format string
// NSString *counter = [NSString stringWithFormat: @"%d", cntr];

// Get get the answer from the question
NSString *answer = [attributeDict valueForKey:XML_TAG_ANSWER_ATTRIBUTE_TEXT];

// example of NSLog
//NSLog(@"question, number is %i, text is %@, obj is %@", question.number, question.questionTxt, question);

// add the question to the answer dictionary
[question.answerHash setObject: answer
forKey: counter ];

// increment counter
cntr++;

}

}



So, it's creating the question, a number that indicates which is the correct answer, and each question has an answer hash. AppState in turn keeps some kind of a collection of questions.

So, what the app looks like it's doing is accessing the AppState collection of questions each time the question controller displays its view, and displaying it on it's page. It's probably incrementing a counter so that the question it displays each time is different.

If we go to the main app delegate, we'll see how this reading process is initiated:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

// Override point for customization after application launch.

NSString *xmlFilePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"questions.xml"];

NSString *xmlFileContents = [NSString stringWithContentsOfFile: xmlFilePath encoding:NSUTF8StringEncoding error:nil];

NSData *data = [NSData dataWithBytes:[xmlFileContents UTF8String] length:[xmlFileContents lengthOfBytesUsingEncoding: NSUTF8StringEncoding]];


XMLReader *xmlReader = [[XMLReader alloc] init];

[xmlReader parseXMLData: data];

self.appState = xmlReader.appState;

appState.currentQuestionNumber = 1;

[xmlReader release];

currentQuestion = [appState currentQuestion];

//navController.viewControllers = [NSArray arrayWithObject:questonViewController];
navController.viewControllers = [NSArray arrayWithObject:startController];

[window addSubview:navController.view];
[self.window makeKeyAndVisible];


return YES;
}




So, this section in particular does the parsing and setting up:

XMLReader *xmlReader = [[XMLReader alloc] init];

[xmlReader parseXMLData: data];

self.appState = xmlReader.appState;

appState.currentQuestionNumber = 1;

[xmlReader release];


Now, when it gets to the QuestionController, it initiates its own variables with the current question:

- (void) viewWillAppear:(BOOL)animated{


// won't be animated on the first call
if (animated) {
appState.currentQuestionNumber++;
[appState saveState];
}

currentQuestion = appState.currentQuestion;

answers = [currentQuestion answerArray];

// triggers reload, see veiwDidLoad method
[tableView reloadData];
}



Which are later used to initialize the display.

Now, the only remaining question is, what increments the question number? That's probably done in the question controller:


// won't be animated on the first call
if (animated) {
appState.currentQuestionNumber++;
[appState saveState];
}


Ok. So, the next challenge is to switch from the XML input to an SQLLite database input. We'll tackle that in the next post.

Saturday, August 27, 2011

Changing the root controller - part 4!



Well, I've delayed the inevitable as long as I can. Today, we'll create the view needed which will eventually become the root controller, opening the question display when a button is pushed.

However, first, as recommended by apple, let's create the controller and its interface, or header file. We'll follow along the same steps discussed in the "HelloWord" tutorial we saw yesterday.

I doesn't help in this case - the file was already created when they created the project. Well, hmm...let's google. I know it sounds elementary, but I wonder is there's a way we can get it to create the nib for us.

Let's check out this link:

http://developer.apple.com/library/ios/#documentation/iphone/conceptual/iPhone101/Articles/03_AddingViewController.html

Adding a View Controller

In this application you’ll need two classes.

Xcode’s application template provided an application delegate class and an instance is created in the nib file.


An instance of the app delegate is created in the nib? So I still don't understand how the kickoff loop works.

You need to implement a view controller class and create an instance of it.


I believe this is what we need to do, since we're replacing the old view controller, no?

Adding a View Controller Class

View controller objects play a central role in most iOS applications. As the name implies, they’re responsible for managing a view, but on iOS they also help with navigation and memory management. You’re not going to use the latter features here, but it’s important to be aware of them for future development. UIKit provides a special class—UIViewController—that encapsulates most of the default behavior you want from a view controller. You have to create a subclass to customize the behavior for your application.


Ah, I see. Let's check question controller; Yes, it subclasses UIViewController. Then again so does the AnswerController. Good. It looks like this may be something pretty standard for anything with a view.

Let's continue.

To add a custom view controller class . . .

In Xcode, in the project organizer select either the project (HelloWorld at the top of the Groups and Files list) or the HelloWorld group folder.

The new files will be added to the current selection.

Choose File > New File and in the New File window.

Select the Cocoa Touch Classes group, then select UIViewController subclass.



Great! This is exactly what I was looking for.


Click Next.

Using the combo box, choose to create a subclass of UIViewController.

Using the checkboxes, make sure that Targeted for iPad is not selected, and that With XIB for user interface is selected.

Click Next.


Make sure the Add to targets check box is selected.

Give the file a new name such as MyViewController.

By convention, class names begin with a capital letter.

Click Save.

Make sure that the files were added to your project.



Good, although I was hoping the nib file would somehow get created. Actually - it was - just at the wrong level, along thing the .m and .h. files. Let's move them to where they belong.


Adding a View Controller Property

You want to make sure that the view controller lasts for the lifetime of the application, so it makes sense to add it as a property of the application delegate (which will also last for the lifetime of the application). (To understand why, consult Memory Management Programming Guide.)

Perform the following tasks in the header file for the application delegate class (HelloWorldAppDelegate.h).
bullet


In my case it's QuizAppDelegate, but ok.

To add a view controller property . . .

Add a forward declaration for the MyViewController class.

Before the interface declaration for HelloWorldAppDelegate, add:

@class MyViewController;

The property will be an instance of the MyViewController class. The compiler will generate an error, though, if you declare the variable but you don’t tell it about the MyViewController class.


Ok, so it's as if we declared a member variable in Java - but it's not defined anywhere. That would be compiler error.

You could import the header file, but typically in Cocoa you instead provide a forward declaration—a promise to the compiler that MyViewController will be defined somewhere else and that it needn’t waste time checking for it now.


It's saying - don't worry, let it go. We'll define it for you later.

(
Doing this also avoids circularities if two classes need to refer to each other and would otherwise include each other’s header files.)


If A needs B's header and vice versa, that's a problem. This avoids it by not parsing through the declaration of B.

Later, you will import the header file itself in the implementation file.


Ah. Use in the implementation, where it's really needed. Not in the above complicated scenarios.

Add a declaration for the view controller property. After the closing brace but before @end, add:
@property (nonatomic, retain) MyViewController *myViewController;


Hmmm. I have a couple of problems. First, there is a little tiny red mark next to my property name in the synthesize. And also, some kind of warning about needing a method.

At this point, I'm going to vary from the script an just start substituting (actually adding alongside) the code from QuestionController for MyViewController

That means, under the interface section in the header, adding a declaration for the new class:

interface QuizAppDelegate : NSObject {
UIWindow *window;
QuestionController *questionViewController;
UINavigationController *navController;
Question *currentQuestion;
AppState *appState;
MyViewController *myViewController;
}


This above isn't mentioned in the tutorial - I dont' know if I'm doing something different the requires this, or what.

Continuing:

Properties are described in the “Declared Properties” chapter in The Objective-C Programming Language. Basically, though, this declaration specifies that an instance of HelloWorldAppDelegate has a property that you can access using the getter and setter methods myViewController and setMyViewController: respectively, and that the instance retains the property (retaining is discussed in more detail later).

To make sure you’re on track, confirm that your HelloWorldAppDelegate class interface file (HelloWorldAppDelegate.h) looks like this (comments are not shown):

#import

@class MyViewController;

@interface HelloWorldAppDelegate : NSObject {

}

@property (nonatomic, retain) IBOutlet UIWindow *window;

@property (nonatomic, retain) MyViewController *myViewController;




Ok - I have the same property declaration, and I have the forward class declaration.

The only thing different is I also have this:

@interface QuizAppDelegate : NSObject {
UIWindow *window;
QuestionController *questionViewController;
UINavigationController *navController;
Question *currentQuestion;
AppState *appState;
MyViewController *myViewController;
}

Hmmm...maybe I should rename this app so I can refer to a copy of my old app. I have a backup, but if I want to experiment with the backup, I should give this or that a new name. But, I dunno - I'm so unfamiliar with IOS, and I had trouble with a rename earlier.

Ok, let's continue - we can always backup the backup and muck around with that.



You can now create an instance of the view controller.
Creating the View Controller Instance

Now that you’ve added the view controller property to the application delegate, you need to actually create an instance of the view controller and set it as the value for the property. To make code-completion work for your new class in Xcode, you also need to import the relevant header file for the view controller.

Perform the following tasks in the implementation file for the application delegate class (HelloWorldAppDelegate.m).
bullet

To import the view controller’s header file . . .

At the top of file, add:

#import "MyViewController.h"


Ok - that's added. It somehow will help code completion work.

Let's keep going.

To add a view controller instance . . .

Add the following code as the first statements in the implementation of the application:didFinishLaunchingWithOptions: method:

MyViewController *aViewController = [[MyViewController alloc]

initWithNibName:@"MyViewController" bundle:nil];

[self setMyViewController:aViewController];

[aViewController release];


And keep going:


Setting Up the View

View controllers are responsible for configuring and managing each “screenful” you see in an application. Each screenful should be managed by a view controller. To codify this principle, a window has a root view controller—the view controller responsible for configuring the view first displayed when the window appears. To get your view controller’s content into the window, you set your view controller to be the window’s root view controller.
bullet



This is good info. The AppDelegate has a window. It also has a root view controller - the view controller responsible for configuring the view displayed when the first window appears. If you want *your* view controller's "screenful" to be seen first, you need to set it to be the windows root view controller.

Continuing:

To set your view controller as the window’s root view controller . . .

Add the following line of code just before the call to makeKeyAndVisible.

self.window.rootViewController = self.myViewController;

Instead of using the dot syntax, you could also use square brackets:

[[self window] setRootViewController:[self myViewController]];

Both lines of code would compile to the same thing; again you can choose which you feel is more readable. A further important point about this line, though, is that it uses the accessor method to retrieve myViewController.


Ok, now, this is a bit of a problem. We are right at the heart of the thorniest part of replacing the root controller.

Here's what the old code with the new code added in below it looks like:


// old code

navController.viewControllers = [NSArray arrayWithObject:questionViewController];

[window addSubview:navController.view];

[self.window makeKeyAndVisible];


// new code

MyViewController *aViewController = [[MyViewController alloc]

initWithNibName:@"MyViewController" bundle:nil];

[self setMyViewController:aViewController];

[aViewController release];

self.window.rootViewController = self.myViewController;

[self.window makeKeyAndVisible];



What a mess! I was thinking I could sort of just inherit the navigation logic once I added the StartController and delegated to that. But I'm really thinking that we have to keep the nav controller at this level, in the app delegate. There was some code in QuestionController that pulled in the nav from app delegate to bring up the answer activity. It's in the main nib.

So, in this case, I think the key must be how to properly negotiate this new view into the nav controller. I don't think we should be setting the root controller of this window to anything else. Btw, is the current code setting the current rootViewController anywhere, I mean, before the logic we just added?

I guess not. But, they all may be magically hooked together in the interface builder.

Right. If I look at "MainWindow.xlb", underneath "Objects", it has an app delegate, a "QuestionController", "Window" - and Navigation Controller.

Ok - what will happen if I just replace it in code?

Sigabort. So, it is probably somehow geting connected in the nib.

Ok, we can add a ViewController object to the objects under the Nav. Then, using property inspector, match it up with the added view. Also, we need to make it an IBOutput. Ok, now we can drag line, and we have the connection! Nice. Run it...yes!

Just in time for dinner.

Here's a look at the new display:



In the next post, we'll tackle have this new view call the QuestionView.