4
votes

I have a UIViewController that materializes its view in loadView (i.e. no nib). Per the documentation (and via confirmation in code), loadView and consequently, viewDidLoad will not get called until UIViewController's view is accessed the first time.

I have another class that instantiates the UIViewController and calls a number of methods on it before the view itself is referenced. Many of these methods modify the view / subviews in the UIViewController. Unfortunately, the view / subviews are not instantiated at this point. (I can easily just add a reference to the view before the other method calls, but this requires effort and understanding of an invisible contract on the user of my UIViewController).

How do I handle methods on the UIViewController which modify the view / subviews? In each of my UIViewController methods, I can check if the view is loaded, but you aren't supposed to call loadView directly. I could put a "self.view;" in the method to presumably assure the view is loaded, but this seems pretty hackish (and of course causes a clang warning).

Additionally, this isn't just a one time thing, since obviously the view could be unloaded during memory events, allowing this state to happen quite often.

I feel like I'm missing something fairly simple, but I haven't been able to find it. What is the appropriate way to handle this state / initialization issue?

2

2 Answers

5
votes

Basically the answer to this kind of thing is to not expose components of your views through your view controller as other objects probably shouldn't be accessing them. So, if your view controller manages a view that renders a person's name, for example, instead of doing something like this:

PersonNameController* pnc = [[PersonNameController alloc] initWithNibName:nil bundle:nil];
[[pnc nameLabel] setText:@"Jason"];
[[self navigationController] pushViewController:pnc animated:YES];

You would separate the data/model bits from the view bits and expose a 'name' property on your view controller. Then you can have this simple example code which always works properly (it requires a bit more code, but it saves you from headaches like this and, to be perfectly honest, is what MVC is about):

PersonNameController* pnc = [[PersonNameController alloc] initWithNibName:nil bundle:nil];
[pnc setName:@"Jason"];
[[self navigationController] pushViewController:pnc animated:YES];

And the implementation of such a controller:

@interface PersonNameController : UIViewController {
 @private
  NSString* name_;
}

// other people use this to pass model data to the controller
@property(nonatomic,copy) NSString* name;   

@end

@interface PersonNameController()

// for us only
@property(nonatomic,assign) UILabel* nameLabel;

@end

@implementation PersonNameController

// properties
@synthesize nameLabel;
@synthesize name = name_;

- (void)setName:(NSString*)value
{
  if( [value isEqualToString:name_] ) return;
  [name_ autorelease];
  name_ = [value copy];
  [[self nameLabel] setText:name_?:@""];
}

// view lifecycle
- (void)loadView
{
  // ob fake frame for now
  UIView* view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
  UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake( 10, 10, 300, 37)];
  [view addSubview:label];
  [self setView:view];
  [self setNameLabel:label];

  // Set the name on the label if one's set already
  [label setText:name_?:@""];

  // clean up
  [view release];
  [label release];
}

// take care of unloads
- (void)viewDidUnload
{
  // no need to release, but set nil so it's not used while the view
  // is unloaded
  nameLabel = nil;
  [super viewDidUnload];
}

- (void)dealloc
{
  // nameLabel is assign (owned by our view) so we don't release here
  // but set to nil just to be good citizens
  nameLabel = nil;
  [name_ release];
  [super dealloc];
}

@end

Quick Note: This was just typed up in this little box and I'm really tired, so it's not checked for total syntax correctness, etc. It's meant as a quick example not something to cut and paste.

Also, if you had a number of properties that needed to be updated each time, you might create a single utility method like -udpateLabels or something that updates them all. Then you call that from each setter and from viewDidLoad or loadView. That way you'll have everything in one place (at the expense of some performance). If, after profiling, you spend too much time in that method, you can break it out as appropriate.

2
votes

Generally, for design reasons, I believe you should not let other class modify UIViewController's view directly (that is why is called UIViewController, because he is the one who controls the view) It's better to tell the UIViewController to modify its view instead.

If the view is not loaded at that time you could just have ivars that save a certain state like:

BOOL viewShrinked;

and use this kind of vars to recover that state later, when the view is loaded or reloaded.

- (void) loadView{
    if(viewShrinked){
       ....
    }else{
        ...
    }
}

BUT: Before doing this: you probably want to revise your code and refactor in a way that calls that affect view are done when the view is available as much as possible.