24

I'm familiar with most of the process of creating an XIB for my own UIView subclass, but not everything is working properly for me - it's mostly to do with the IBOutlets linking up. I can get them to work in what seems like a roundabout way.

My setup is this:

  • I have MyClass.h and MyClass.m. They have IBOutlets for a UIView (called view) and a UILabel (called myLabel). I added the 'view' property because some examples online seemed to suggest that you need this, and it actually solved an issue where I was getting a crash because it couldn't find the view property, I guess not even in the UIView parent class.
  • I have an XIB file called MyClass.xib, and its File's Owner custom class is MyClass, which prefilled correctly after my .h and .m for that class existed.

My init method is where I'm having issues.

I tried to use the NSBundle mainBundle's 'loadNibNamed' method and set the owner to 'self', hoping that I'd be creating an instance of the view and it'd automatically get its outlets matched to the ones in my class (I know how to do this and I'm careful with it). I then thought I'd want to make 'self' equal to the subview at index 0 in that nib, rather than doing

self = [super init];

or anything like that.

I sense that I'm doing things wrong here, but examples online have had similar things going on in the init method, but they assign that subview 0 to the view property and add it as a child - but is that not then a total of two MyClass instances? One essentially unlinked to IBOutlets, containing the child MyClass instantiated via loadNibNamed? Or at best, is it not a MyClass instance with an extra intermediary UIView containing all the IBOutlets I originally wanted as direct children of MyClass? That poses a slight annoyance when it comes to doing things like instanceOfMyClass.frame.size.width, as it returns 0, when the child UIView that's been introduced returns the real frame size I was looking for.

Is the thing I'm doing wrong that I'm messing with loadNibNamed inside an init method? Should I be doing something more like this?

MyClass *instance = [[MyClass alloc] init];
[[NSBundle mainBundle] loadNibNamed:@"MyClass" owner:instance options:nil];  

Or like this?

MyClass *instance = [[[NSBundle mainBundle] loadNibNamed:@"MyClass" owner:nil options:nil] objectAtIndex:0]; 

Thanks in advance for any assitance.

Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
Cloov
  • 538
  • 1
  • 3
  • 15
  • At one point you say you're creating a UIView subclass and then write about having IBOutlets inside it to connect to views and labels. That's rather unusual. Normally, you create a view controller and give it the views to manage, whether stock or custom. – Phillip Mills Nov 23 '12 at 19:15
  • Your first example does create two instances of `MyClass` which is probably not what you want. Unfortunately I'm not sure what to suggest here because your use of `MyClass` is unclear. How do you intend to use `MyClass`? Specifically what were you doing that you saw "a crash because it couldn't find the view property"? That makes me suspect that you have some confusion between views and controllers here. – Jonah Nov 23 '12 at 19:17

3 Answers3

23

The second option is the correct one. The most defensive code you could do is like this:

+ (id)loadNibNamed:(NSString *)nibName ofClass:(Class)objClass {
    if (nibName && objClass) {
        NSArray *objects = [[NSBundle mainBundle] loadNibNamed:nibName 
                                                         owner:nil 
                                                       options:nil];            
        for (id currentObject in objects ){
            if ([currentObject isKindOfClass:objClass])
                return currentObject;
        }
    }

    return nil;
}

And call like this:

MyClass *myClassInstance = [Utility loadNibNamed:@"the_nib_name" 
                                         ofClass:[MyClass class]]; 
// In my case, the code is in a Utility class, you should 
// put it wherever it fits best

I'm assuming your MyClass is a subclass of UIView? If that's the case, then you need to make sure that the UIView of your .xib is actually of MyClass class. That is defined on the third Tab on the right-part in the interface builder, after you select the view

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Ismael
  • 3,927
  • 3
  • 15
  • 23
  • I'm relatively new to working with nibs and have seen this pattern (load nib, pluck out a single object from the returned array, carry on) numerous times. It seems very inefficient in that it creates a number of instances of objects then discards all but one. e.g., if I have a nib containing 5 "things" and I follow the pattern above to create instances for each one, I've basically created 25 instances and thrown away 20. Seems to make more sense to bite the bullet and create 5 separate nibs. Saves cycles as well as defensive code like that above. – Lee Fastenau Mar 26 '15 at 01:37
  • Well, yes, this is intended for the use of a single NIB of a single UIView class. I don't see where I suggested otherwise. If you have a NIB with multiple views, you would have to tweak the method to find each view correspondingly – Ismael Mar 26 '15 at 14:08
7

All you need to do is create the subview via loadNibNamed, set the frame, and add it to the subview. For example, I'm adding three subviews using my MyView class, which is a UIView subclass whose interface is defined in a NIB, MyView.xib:

So, I define initWithFrame for my UIView subclass:

- (id)initWithFrame:(CGRect)frame
{
    NSLog(@"%s", __FUNCTION__);

    self = [super initWithFrame:frame];
    if (self)
    {
        NSArray *nibContents = 
          [[NSBundle mainBundle] loadNibNamed:@"MyView" 
                                        owner:self 
                                      options:nil];
        [self addSubview:nibContents[0]];
    }
    return self;
}

So, for example, in my UIViewController, I can load a couple of these subclassed UIView objects like so:

for (NSInteger i = 0; i < 3; i++)
{
    CGRect frame = CGRectMake(0.0, i * 100.0 + 75.0, 320.0, 100.0);
    MyView *myView = [[MyView alloc] initWithFrame:frame];
    [self.view addSubview:myView];

    // if you want, do something with it: 
    // Here I'm initializing a text field and label

    myView.textField.text = [NSString stringWithFormat:@"MyView textfield #%d",
                              i + 1];
    myView.label.text = [NSString stringWithFormat:@"MyView label #%d", 
                          i + 1];
}

I originally advised the use controllers, and I'll keep that answer below for historical reference.


Original answer:

I don't see any references to view controllers here. Usually you'd have a subclass of UIViewController, which you would then instantiate with

MyClassViewController *controller = 
  [[MyClassViewController alloc] initWithNibName:@"MyClass" 
                                          bundle:nil];

// then you can do stuff like
//
// [self presentViewController:controller animated:YES completion:nil];

The NIB file, MyClass.xib, could specify that the base class for the UIView, if you want, where you have all of the view related code (e.g. assuming that MyClass was a subclass of UIView).

Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Thanks. I was avoiding the use of a controller because essentially I'm building up a fairly complex UI from subclasses in a one-to-many relationship, and so I saw MyClass as being the view that is replicated several times. Since I'm not doing too much with these other than populating them once, I thought it best to treat them as an array and populate them from one parent 'controller' (i.e. a ViewController). – Cloov Nov 23 '12 at 19:53
  • Thanks again! In your example where you use MyView *myView = [[MyView alloc] initWithFrame:frame], does that automatically get constructed using the XIB file? Your first example that uses [[NSBundle mainBundle] loadNibNamed:@"MyView" owner:self options:nil] might work for me as long as (a) I can typecast it to MyView and work with it a little before just adding it as a subview, and (b) that by being constructed via loadNibNamed, it'll call an init metho that I can override to do my MyView.m-file based configuration. – Cloov Nov 23 '12 at 23:15
  • @Cloov I think I wasn't clear. I'm suggesting that you (a) define a version of `initWithFrame` for your subclassed `UIView` that loads the view from the NIB; and (b) your view controller (or whatever) that now wants to use your subclassed `UIView` only has to call `initWithFrame`, and the method we wrote in step (a) will take care of loading it from the NIB. – Rob Nov 23 '12 at 23:21
  • This solution results in two objects being instantiated, one owned by the parent controller and one created inside `initWithFrame`. Be sure to keep a reference to the view that you are loading from the nib if you want to access any outlets. – Andrew Aarestad Aug 28 '14 at 13:51
  • 1
    To avoid the "two views being instantiated" problem, you can use: `return nibContents[0];` in `initWithFrame` to use the view loaded from Nib only. – Andrew Aarestad Aug 28 '14 at 14:03
  • By design nibs load their controlling files not the other way around. I think programmer intention is much clearer when we make the caller instantiate the view from a nib by saying so explicitly. – SmileBot May 08 '17 at 13:45
2

Here's one method that I use:

  1. Create a subclass for UIView, this will be called MyClass
  2. Create a view xib file. Open in interface builder, click File's Owner and in the Identity Inspector, change the class to that of your parent view controller, e.g. ParentViewController.
  3. Click the view already in the list of objects and change it's class in Identity Inspector to MyClass.
  4. Any outlets/actions that you declare in MyClass will be connected by click-dragging from View (not File's Owner). If you want to connect them to variables from ParentViewController then click-drag from File's Owner.
  5. Now in your ParentViewController you need to declare an instance variable for MyClass.

ParentViewController.h add the following:

@class MyClass

@interface ParentViewController : UIViewController {
    MyClass *myClass;
}

@property (strong, nonatomic) MyClass *myClass;

Synthesize this in your implementation and add the following in your viewDidLoad method:

- (void)viewDidLoad {
    [super viewDidLoad];

    [[NSBundle mainBundle] loadNibNamed:@"MyClass" owner:self options:nil];
    self.myClass.frame = CGRectMake(X,Y,W,H); //put your values in.
    [self.view addSubview:self.myClass];
}
sooper
  • 5,991
  • 6
  • 40
  • 65
  • Thanks, I'm going to try this - it might work for me too. I'd want to not tie the File's Owner to a UIViewController if possible, but I'm going to see if I get the configuration I want by adding the classname to the View item in IB and then link IBOutlets there. – Cloov Nov 23 '12 at 23:17