How does loadNibNamed work? UIView outlets not initializing using loadNibNamed

16.1k views Asked by At

I know this is quite straight forward but after too much hair-pulling I am nowhere near solution.

I have seen tutorials explaining how to create view using XIB and all. But none of them address the situation that I have here.

I have an XIB file, a custom UIView subclass that has few labels and buttons. The UIView subclass is reusable, and that is the reason I can't have outlets inside any single View controller. As a result I store individual controls (subviews) of this view inside my custom UIView itself. This is logical, as no view controller should own the subviews of this custom view which is to be included in every view controller.

The problem is, I don't know how to initialize the entire UI fully.

Here is my code for UIView Subclass:

@interface MCPTGenericView : UIView

+(id)createInstance : (bool) bPortrait;

@property (weak, nonatomic) IBOutlet UIView *topView;
@property (weak, nonatomic) IBOutlet UIView *titleView;
@property (weak, nonatomic) IBOutlet UILabel *titleLabel;
@property (weak, nonatomic) IBOutlet UIButton *logoButton;
@property (weak, nonatomic) IBOutlet UITextField *searchTextField;
@property (weak, nonatomic) IBOutlet UIButton *menuButton;
@end

Later on, I also plan to use this same XIB file for landscape orientation of this UIView too, and I plan to use the same above outlets with landscape oriented controls in same XIB.

And here is the implementation:

@implementation MCPTGenericView
//@synthesize topView, titleLabel, titleView;

+(id)createInstance : (bool) bPortrait
{
    UIView * topLevelView = nil;
    MCPTGenericView * instance = [MCPTGenericView new];
    NSArray * views = [[NSBundle mainBundle] loadNibNamed:@"MoceptGenericView" owner:instance options:nil];

    int baseTag = (bPortrait)?PORTRAIT_VIEW_TAG_OFFSET:LANDSCAPE_VIEW_TAG_OFFSET;

    // make sure customView is not nil or the wrong class!

   for (UIView * view in views)
   {

       if (view.tag == baseTag)
       {
           topLevelView = view;
           break;
       }
   }

    instance.topView = (MCPTGenericView *)[topLevelView viewWithTag:baseTag + 1];
    instance.searchTextField = (UITextField *)[topLevelView viewWithTag:baseTag + 2];
    instance.menuButton = (UIButton *)[topLevelView viewWithTag:baseTag + 3];
    instance.logoButton = (UIButton *)[topLevelView viewWithTag:baseTag + 4];
    instance.titleView = [topLevelView viewWithTag:baseTag + 5];
    instance.titleLabel = (UILabel *)[topLevelView viewWithTag:baseTag + 6];

    return instance;
}

-(id)initWithCoder:(NSCoder *)aDecoder
{
    if ((self = [super initWithCoder:aDecoder]))
    {
        [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"MCPTGenericView" owner:self options:nil] objectAtIndex:0]];
    }
    return self;
}

-(void)awakeFromNib
{
    [super awakeFromNib];
    [self addSubview: self.titleView];
    [self addSubview:self.topView];
}

- (id) init
{
    self = [super init];
    if (self)
    {
        [[NSBundle mainBundle] loadNibNamed:@"MCPTGenericView" owner:self options:nil];
        [self addSubview:self.topView];
        [self addSubview:self.titleView];

    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self)
    {
        // Initialization code
         [[NSBundle mainBundle] loadNibNamed:@"MCPTGenericView" owner:self options:nil];
        [self addSubview:self.topView];
        [self addSubview:self.titleView];
    }
    return self;
}

/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
    // Drawing code
}
*/

@end

Something that worked:

I succeeded in calling initWithFrame:frame from my viewcontroller. That way, I could see all controls properly initialized. But then, why should I be supplying a frame if I have already drawn an XIB? Shouldn't loadNibNamed be handling frame setting and layout stuff since that is the intended use of XIBs?

I am also baffled at the way loadNibNamed needs an owner object. Why do we already need an object to get the same object from XIB? That too, a half-baked one?

Please help...

2

There are 2 answers

5
Nirav Bhatt On BEST ANSWER

What was baffling me was the way loadnibnamed loses xib layout & outlet information. I finally found a way to achieve it.

Here is a recap of what works:

1) Suppose MyCustomView is your custom view class - you design it and its subviews as part of XIBs. You do this via interface builder, so self-explanatory.

2) Add MyCustomView.h and MyCustomView.m (boilerplate) via Xcode -> File -> New -> Objective C Class.

3) Next, within MyCustomView.xib, set File's Owner = MyCustomView (class name just added). Do not touch top most View's custom class - leave it as UIView. Else it will end up in recursion!!!

4) In MyCustomView.h, create few outlets corresponding to subviews within MyCustomView.xib.

Such as:

@property (weak)  IBOutlet UILabel * label1;
@property (weak)  IBOutlet UIButton * button1;

5) Go to MyCustomView.xib. Select each subview (label, button), right click, drag from "New Referencing Outlet" and drag it up to File's Owner.

This will popup a list of outlets matching the subview's type from where you have dragged. If you dragged from a label, it will pop up label1, and so on. This shows that all you did up to this step is correct.

If you, on the other hand, screwed up in any step, no popup will appear. Check steps, especially 3 & 4.

If you do not perform this step correctly, Xcode will welcome you will following exception:

setValue:forUndefinedKey: this class is not key value coding-compliant for the key

6) In your MyCustomView.m, paste / overwrite following code:

-(id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];

    if (self)
    {
        NSString * nibName = @"MyCustomView";
        [[[NSBundle mainBundle] loadNibNamed:nibName owner:self options:nil] firstObject];

        [self addSubview:self.labelContinentName];
    }
    return self;
}

This step is crucial - it sets your outlet values (label1, button1) from nil to tangible subviews, and most importantly, sets their frame according to what you have set within MyCustomView.xib.

7) In your storyboard file, add view of type MyCustomView - just like any other view:

  • Drag a UIView in your View Controller main view rectangle
  • Select the newly added view
  • In Utilities -> Identity Inspector, set custom class value = MyCustomView.

It should be up & running no problem!

2
Rukshan On

loadNibNamed does not handle frame setting, it only loads content and makes the objecet available to your code. initWithFrame: must be called to insert a new object to the view heirarchy of a window.