Sunday, August 28, 2011

Next step - reconnecting the question controller!

So, we happily managed to get our new view controller to replace the old one as the root controller named in the navigator. The next step is clearly to get that controller to bring up the question activity which it just displaced. I think we have a good example of that. The question controller itself brings up the answer controller when a detail is selected. It does so doing this:

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

AnswersController *answersController =
[[ AnswersController alloc] initWithIndexPath:indexPath];

[delegate.navController pushViewController:answersController animated:YES];
[answersController release];



One question I have - why am I releasing the answer controller - I know if you allocate something, you need to release it. So, I guess ownership has passed to the navController.

So, we want to do something like this on the button push. So, we need to connect the button push with some kind of callback in the code. We just did this the hello world tutorial we looked at a few posts ago.

Ah, here it is. In the interface / header:

@property (nonatomic, retain) IBOutlet UILabel *textLabel;

- (IBAction)changeTheTextOfTheLabel;

@end


And in the implementation:



- (IBAction)changeTheTextOfTheLabel
{
[textLabel setText:@"Hello, World!"];
}



Naturally, that gives rise to missing variable errors. I'm trying to figure out the best way to declare them. If I go back to the QuestionController, for whatever reason I've imported the headers into the implementation class, as opposed to the interface. Which is better? I'd like to know. But for now I'm going to stick with the example.

#import "QuizAppDelegate.h"
#import "Question.h"

Ok, it resolves it sort of. But it says it's a forward declaration which may not exist?

Actually, I should refer to the app delegate, as that's the one which delegates to QuestionController originally.

In the end, here's how the interface looks:




#import

@class QuestionController; // forward declaration;
// import in implementation

@interface StartController : UIViewController {

// declare the controller
QuestionController *questionViewController;

}

// name the controller as a property

@property (nonatomic, retain) IBOutlet QuestionController *questionViewController;

// declare the method
- (IBAction) openQuestionController;

@end




And here's what's been added to the implementation



#import "StartController.h"
#import "QuizAppDelegate.h"
#import "QuestionController.h"


And:




@synthesize questionViewController;


- (IBAction)changeTheTextOfTheLabel
{
QuizAppDelegate *delegate =
(QuizAppDelegate *)[[UIApplication sharedApplication] delegate];

delegate.navController.viewControllers = [NSArray arrayWithObject:questionViewController];

[delegate.navController pushViewController:questionViewController animated:YES];
[questionViewController release];
}




Ok, that's a lot of magic just to get it to compile. The big question is: does it need to be specified in the interface builder? Thinking to Android, Android gets access to to its layouts via the "R" resource. In iOS, it's by the IBOutlet and callback. But this is an entirely different view. Still, the controller needs access to access it somehow. For example, when we added the new controller for the quiz app delegate, we needed to add the a view controller to the main nib file, and then connect it to the main window as an outlet.

Btw, how does the controller get access to its nib in the first place? Ah, well, this might help. If you go to the interface builder, and inspect properties on the file owner, you will see the view controller subclass it's connected to.

Before we get to that, we should connect up our button to the method we've created for it.

Hmm...I'm having trouble with that. When I right click on the button, and try to add the IBAction, nothing shows up. Oh, I see - you need to drag the line to the fileOwner and *then* choose.

Ok, there was another problem, which was that I had renamed it, and that confuses Xcode 4. I've put it back to it's original name for now.

Now, let's run it. And, sure enough we get a sigabort. So, let's go back and make the QuestionController available to the StartController.

There might be a couple of ways to do this. I saw something about InitWithNib in the hello world. I can't use the original example because it's some kind of "initFromIndex" or something, which has to do with selecting a row.

So, I can choose between the other two techniques I've used. The first, used in the app delegate, has the target view as an object on its view. It then connects to it via an IBOutlet:

@property (nonatomic, retain) IBOutlet StartController *myViewController;

And then calls it like this:


navController.viewControllers = [NSArray arrayWithObject:myViewController];
[window addSubview:navController.view];


However, I'm not sure this is the best way to do it. I'd prefer use the question's method of passing control:



[delegate.navController pushViewController:answersController animated:YES];



The big question is, do I have to initialize it somehow, or will it be okay just to have the variable reference as is? What does "arrayWithObject" do, anyway?

According to the docs, it looks like arrayWithObject *creates* the array, but it doesn't seem to do anything with creating the object.

Creates and returns an array containing a given object.

+ (id)arrayWithObject:(id)anObject


So, let's try that way first.

Ok, I already have the question controller all set up in exactly the same way it was set up with the app delegate. So, now it's just a question of connecting question activity to the IB outlet in interface builder.

Good. Right click on the file owner, go to the question controller's IBOutlet, and then just drag it to the QuestionController object, which I just added to the object.

Ok, now let's run it, click the button - and see what happens.

Sugar. It's a Sigabort.

Here's the error message:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: 'Pushing the same view controller instance more than once is not supported ()'

Hmm...


Ok, this turned out to be the magic sauce:



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

//QuestionController *questionViewController = [[QuestionController alloc] initWithNibName:@"RootController" bundle:nil];

//delegate.navController.viewControllers = [NSArray arrayWithObject:questionViewController];

[delegate.navController pushViewController:questionViewController animated:YES];
[questionViewController release];



So, when I commented out the NSArray statement, it worked. I must be doing it already in the app delegate? No. I need to figure that one out.

Anyway, it's working :)















No comments:

Post a Comment