Subclass of UIViewController initialising itself from a .xib can't use UINavigationItem? - iphone

I've finally had to give up the relentless search for an answer to this question, as I just can't find anyone that's asked it before! So hope someone can provide some insight. I'll start by explaining what I can do, then compare that with what I can't figure out how to do.
Suppose I have a custom VC called RootViewController. It contains an outlet of type MyViewController. My RootViewController has an .xib which contains a generic VC object dragged out of IB's palette which is given the class type of MyViewController, I set the right bar button item to a UIBarButtonItem called 'Cancel'. The RootViewController unarchives it's .xib, hooks up the outlet to the MyViewController object, I push said object on to the navigation stack which displays as expected, I have a view and a button on the navigation bar that says 'Cancel'.
The problem I have with this approach is that I want MyViewController to be re-usable across any object that might want to create a MyViewController. This therefore means that the "File's Owner" could be any object. Previously it was RootViewController, but what if another object wanted to instantiate a MyViewController? The Files Owner will be different every time. I also want it to be able to completely initialise itself from a nicely self-contained .xib. Basically, I want to do this:
MyViewController *myVC = [[MyViewController alloc] init];
And in the implementation of MyViewController, I write this:
- (id)init
{
if ( self = [super initWithNibName:#"MyViewController" bundle:nil] )
{
// Initialisation
}
}
This neatly hides the name of the .xib used to intialise the VC, uses all the goodness IB gives me in configuring the controller and view, and with MyViewController always being the owner, it means I solve the File's Owner problem too - it will work for any type of class that creates it.
So, in order to achieve this, I create a .xib, set the File's Owner to be of type MyViewController, drop in a UINavigationItem and add the UIBarButtonItem. I now have a .xib structurally the same as before, except it does not use IB's generic VC object as separate top level object, the .xib is the VC definition, rather than something that contains a VC definition.
So, given that File's Owner is a MyViewController and as such a subclass of UIViewController (supposedly inheriting everything you get by using a UIViewController from IB's palette), then I should inherit all the functionality of it's superclass... Except it doesn't.
When the .xib in unarchived, it does not hook up the UINavigationItem. Therefore, when it's pushed on to the navigation stack, none of the bar button items are displayed.
UIViewControllers navigationItem property is read-only, creating an outlet for it in iPhone OS 3.0 is therefore deprecated.
So, at the end of all this, how on earth does the nib loading code manage to connect IB's version of the VC object to the navigation item? And why, even though my object is a UIViewController through inheritance, will it not do it for my object? I am completely at a loss to fathom this out...
Thanks for reading this far! Hope to hear from you guru's

Only a UINavigationController sets UIViewController's navigationItem property. Do something like this:
MyViewController *viewController = [[MyViewController alloc] init];
[self.navigationController pushViewController:viewController animated:YES];
[viewController release];
Assuming you are in a view controller that already has a navigation controller. After you have pushed it, you will be able to access navigationItem from inside MyViewController.

Right, I think I get it now. I thought I'd post an answer to my own question as I've seen at least one other person ask a similar question, and both seem to point to the same conclusion, so I hope this will be of help to others.
Essentially (and this will sound obvious) a UIViewController is not a Proxy Object! It's not instantly obvious as we're all used to the idea that if any two objects inherit from the same base class, then their implementations are the same and they will behave in exactly the same way (assuming no customisation in the inheriting class). But an IB object's type is distinct from the class attribute you can assign to these objects.
They are not both UIViewController objects because their class attribute it set in such a way.
Simply setting the class attribute of these objects in IB does not mean that these objects are now UIViewController objects. A View Controller object remains a View Controler object, and a Proxy Object remains a Proxy Object. As far as IB is concerned, they are both very different beasts, they just happen to have the same class attribute.
Just take a look in to the .xib and you'll find your IB View Controller objects have been archived like this:
<object class="IBUIViewController" id="...">
...
</object>
And the Proxy Object (that is set to a subclass of a UIViewController) is archived like this:
<object class="IBProxyObject" id="...">
...
</object>
As you can see they are both very different types, one is a IBUIViewController and the other is a IBProxyObject - and then it starts to makes sense - you can't impose VC controller attributes on an object of type IBProxyObject.
It is also interesting to note the class type is an IBUIViewController object and not just a UIViewController object. Whether this is just a naming convention or not I don't know, but it could also imply that IB's view controller objects wrap the instantiated VC object, or is a factory object for it. For example, you can set a "Resize View From NIB" attribute in IB's view controller object - but I can find no equivalent property or methods in the UIViewController reference docs.
So in conclusion, if you're trying to instantiate an object programatically instead of using outlets to an IB object, be prepared to implement some of the initialisation that the IB version would otherwise provide for you...

Related

Two UIViewController, one XIB File

Let's say, I have three UIViewController
UserFormViewContoller
NewUserFormViewController : UserFormViewController
UpdateUserFormViewController : UserFormViewController
So, NewUserFormViewController and UpdateUserFormViewController view controller inherit from it's parent to share the basic functionality. The different will be their method, create and update.
The views also have a lot of things in common, almost everything. The different view components between NewUserFormViewController and UpdateUserFormViewController is a button to perform save task (create or update)
Is it possible to have two UIViewController sharing one XIB file? Let's say, UserFormViewController.xib and then I do
[[NewUserFormViewController alloc] initWithNibName#"UserFormViewController" bundle:nil];
[[UpdateUserFormViewController alloc] initWithNibName#"UserFormViewController" bundle:nil];
The other question but important is, when I edit xib file with Interface Builder, what owner's reference outlets and IBActions is it talking about, NewUserFormViewController or UpdateUserFormViewController?
(IBActions and Outlets showing when we right click at the Placeholders -> File's Owner)
If that's so, I will just use one XIB file and programmatically add other specific view component (It would be great to have only one XIB file so that I can make some changes at a place but effective on both)
The "file owner" is just a convention so that XCode can show you the correct IBOutlets and IBActions in its inspectors. If you create a generic (in OO terms: abstract) UserFormViewController (.h, .m, .xib), wire it in IB; then subclass it in two NewUserFormViewController and UpdateUserFormViewController, they'll inherit their outlets and actions from their parent class without any problem.
#Armaan I met the same problem by simply call subclass' alloc init method. I fixed this problem by using initWithNibName: and supply xib file name of parent class.

I can make UINavigationController load only at 2nd level, not at Root View Controller

I tried looking for a similar problem but I could not find a similar question.
I am loading a UINavigationController in a UIView which is not (as in most examples) the MainWindow.
I created one new .xib called DocumentsViewController which is subclass of UIView (it has the related .m and .h files). And I created a DocumentsRootViewController.xib, which is a subclass of UITableViewController, which is supposed to be the UINavigationController's RootViewController.
I moved to DocumentsViewController and added a UINavigationController object in Interface Builder. Then I went to code, and added it as in IBOutlet, and connected it to the object.
In the ViewDidLoad, I execute the following lines:
DocumentsRootViewController *rootViewController = [[[DocumentsRootViewController alloc] init] autorelease];
rootViewController.title = #"Documents";
[navigationControllerDocuments initWithRootViewController:rootViewController];
[self.view addSubview:navigationControllerDocuments.view];
It shows the table as intended, but it shows a "Back" button to the "Root View Controller" (as in picture below).
Why? Shouldn't it already know that the rootviewcontroller has been set?
Thank you in advance to the ones that clarify this doubt
Giovanni
When you add the UINavigationController via the Nib it actually creates an instance of a UINavigationController inside the nib file with a default RootViewController set (of type UIViewController) and with a default title of RootViewController.
When you load the nib, this object is being created as part of loading the nib (i.e when you initialise DocumentsViewController) - so the navigationControllerDocuments outlet is already initialised as a UINavigationController with the default ViewController already set on it.
What I think is happening is when you call 'initWithRootViewController' - you are calling this on an already initialised object - so it is running the initialisation code again - pushing the second view controller (the DocumentRootViewController) onto the stack, but the default one that was created in the nib is already there.
What you should probably do is forget about creating one in the nib and initialise the whole thing programatically.
i.e. where you do:
[navigationControllerDocuments initWithRootViewController:rootViewController];
I suggest that you do an alloc and init instead:
[[navigationControllerDocuments alloc] initWithRootViewController:rootViewController];
Since you are doing this you really don't need to have the navigation controller added to the nib so if this works you should remove it from the nib since you are replacing it with this one in code.

Trying to update UILabel with contents of UITextField on different view controller

I have two view controllers, one is MainViewController, the other is SetupViewController. I want a UILabel on MainViewController to set the text to the contents of a UITextField from the SetupViewController when a button is pressed in the SetupViewController.
In SetupViewController, I have this in the IBAction:
- (IBAction)donePressed:(id)sender {
MainViewController *mvc = [[MainViewController alloc] init];
[mvc.testLabelOnMVC setText:testTextFieldOnSVC.text];
[release mvc];
}
testLabelOnMVC (and testTextFieldOnSCV, with respective terms) is
#property (nonatomic, retain) UILabel *testLabelOnMVC;
and is also synthesized.
Every time I try, it doesn't work. Nothing happens, nothing changes. I have no errors or warnings. Can anyone help me out?
The view of your MainViewController does not exist until you reference the MainViewController's view property (which forces viewDidLoad to execute). You must reference the view (or otherwise force the view to be constructed) before you attempt to modify any UI objects in the MainViewController.
You are allocating a new MainViewController when you press the button, then you are setting the text of the label on this new controller, not on the MainViewController that your app is showing.
To fix this, create either and IBOutlet or iVar that points to the original MainViewController and set the text on that instead.
Easiest way is to create a #property in the main view controller and write the text in there. Then just read it in the second MVC's viewDidLoad.
The only views that MainViewController should worry about are the ones that it owns; it shouldn't be trying to access the view hierarchy managed by SetupViewController. Likewise, SetupViewController should not directly modify views in MainViewController's view graph.
The right way to do what you're asking is for the two controllers to talk to each other, either directly or via the data model. For example, let's say that your MainViewController instantiates SetupViewController. If that's the case, it'd be natural for mvc to set itself as svc's delegate, so that svc sends it a messages like -setupController:didUpdateTestStringTo:. MainViewController's implementation of that method could then save the new test string somewhere and update it's testLabel field.
Another example: MainViewController instantiates SetupViewController. SetupViewController contains a field where the user can enter a new value for the test string. Before exiting, SetupViewController writes the contents of that field into NSUserDefaults or some other common data storage. When control returns to MainViewController, that object reads the shared data and updates itself as necessary, including setting the new value for testLabel.
There are other variations on the same theme, but the common thread here is that neither view controller has to directly access views that it doesn't own.
You can change the text of the label if the view is already loaded. Instead of initializing the viewcontroller, retrieve it from the view stack if you are using navigation controller.
I dont know if your viewController is already loaded or not.

Adding a variable to all UIViewControllers

I have this class (let's call it FooViewController) that's a subclass of UIViewController. It's supposed to act similarly to a UINavigationController, in that there's a rootController and you can add other UIViewControllers to it. And each UIViewController within FooViewController can create another UIViewController and push that new UIViewController to FooViewController.
Here's an example of what I mean. This is the auto-created code when you add a new UITableViewController to your project.
DetailViewController *detailViewController = [[DetailViewController alloc] initWithNibName:#"Nib name" bundle:nil];
// ...
// Pass the selected object to the new view controller.
[self.navigationController pushViewController:detailViewController animated:YES];
[detailViewController release];
Here's the question: How can I have a variable like navigationController in all of my UIViewControllers? Just like you can add methods to existing classes using categories, can you also add variables to existing classes? It sounds like Associative References is what I'm looking for, but it's only available on a Mac.
One solution that would work for me is to subclass all the UIViewControllers that I might use, and have my actual classes subclass off of those. For example, I might do:
#interface FooUIViewController : UIViewController {
FooViewController *fooViewController;
}
#interface FooUITableViewController : UITableViewController {
FooViewController *fooViewController;
}
So that I could do: [self.fooViewController pushViewController:detailViewController];. But this seems like a dirty way of doing it.
I feel this shouldn't be a difficult thing to do and maybe I'm thinking about it wrong. Any thoughts? Thanks!
No, you can't add an instance variable to UIViewController.
Subclassing is not a bad way of doing it, considering more things it can do in relation to FooViewController. It doesn't harm the design of your classes because they depend on FooViewController anyway (just as the same as UIViewController having navigationController property).
Another way of doing it is to access the FooViewController object via the application delegate. But I think this is a dirty way of doing it (because they now depend on your application delegate).

UIViewController created with initWithNibName: bundle: or via an IBOutlet behave differently

I found a strange behavior, and would like to be explained what assertion I am making that is wrong.
In an AppDelegate class of a freshly created WindowBased project, I am adding a UIViewController to the window.
I can do it two different ways:
- with an IBOutlet. In IB, I simply instanced an UIViewController, set its class to TestViewController and connected it (scenario A of the code).
- creating the UIViewController with code (scenario B).
- (void)applicationDidFinishLaunching:(UIApplication *)application {
#define USE_IBOUTLET YES // Comment this line to switch to scenario B
#ifdef USE_IBOUTLET
// Scenario A
[window addSubview:theTestViewController.view];
[window makeKeyAndVisible];
#endif
#ifndef USE_IBOUTLET
// Scenario B
TestViewController *theTestViewControllerProgrammatically;
theTestViewControllerProgrammatically = [[TestViewController alloc] initWithNibName:nil bundle:nil];
// According to Apple: "It is a good idea to set the view's frame before adding it to a window.", so let's do it
[theTestViewControllerProgrammatically.view setFrame:[[UIScreen mainScreen] applicationFrame]];
[window addSubview:theTestViewControllerProgrammatically.view];
[window makeKeyAndVisible];
#endif
}
As I did not do any customization of the object in IB, I should have the same behavior in both scenario.
Scenario A, using the IBOutlet works as expected.
But the scenario B has the following problems:
- The view is not at the right position (20 pixels to high, and covered by the status bar).
- The view doesn't resize properly (for example, try to toggle the In Call Status bar)
Why?
Zip archive of the project here if you want to reproduce the problem: http://dl.dropbox.com/u/1899122/code/ProtoWindowBasedStrangeness.zip
This is going to sound really silly after my long-winded answers, but the problem you're having is simple to fix (programatically).
This line:
[theTestViewController.view setFrame:[[UIScreen mainScreen] applicationFrame]];
Should actually be:
[theTestViewControllerProgrammaticaly setFrame:[[UIScreen mainScreen] applicationFrame]];
You were setting the frame for the VC set by IB, not by the one you created programatically.
Anyway - it's worth noting that all my comments still apply! There are still a few things you'll have to do programmatically if you don't use IB's controller objects (for example, setting up the navigation bar items)
Paul
I have been having a very similar problem to you in that I noticed VC objects are not all created equal! The problem I'm having is setting the navigation bar items, I just can't seem to do it when File's Owner is a view controller object that I instantiate programatically. It only works if I unarchive IB's controller objects.
I downloaded your project and had a play around with it, and it got me thinking some more about what might be going on. I think I can provide a reasonable answer, but not sure if there is a simple solution...
What I believe is going on is that Apple have created these controller objects in IB that are slightly more specialised. One suggestion this might be true is that IB VC objects have an attribute you can set that has no direct corresponding property for a UIViewController class that I can see, so IB's controller objects may have some additional functionality that non-IB UIViewController subclasses can't take advantage of. Given that objects in an .xib are complete 'freeze-dried' objects, Apple may have included all kinds of private attributes we can't see or use in their IB versions of them - this may have some effect on how the objects are initialised.
For example, in your MainWindow.xib, select the IB VC object and you can set attributes on it from the Inspector Palette, such as "Resize View From NIB". If you un-check this and re-run your app, you'll see the VC appear exactly as it does in scenario B. As you can't check this item when from the File's Owner attributes (even though it is as a UIViewController), you're unable to take advantage of whatever is being done by the view controller to give you the behaviour you want.
The result of this is that when you use TestViewController.xib to initialise your VC object in code, none of the IB specific attributes of a VC are set, therefore a bog-standard UIViewController is created, and so things like the "Resize View From NIB" attribute and setting up the navigation items have to be implemented yourself.
I've not yet found a way to take advantage of the functionality that IB's view controllers have when I instantiate them using initWithNibName:bundle:nibBundle (I'm guessing it's all private stuff we can't access), but hopefully this might have given you a starting point...
Of course, I could be completely wrong and someone will make me look like a complete idiot!
Paul
Probably in case B that view is not aware of the presence of a status bar. You need to resize it accordingly and adjust its position to take the status bar into account. That is done by changing the frame (size) and bounds (location) properties of a UIView.