iOS loadNibNamed confusion, what is best practice?
Asked Answered
L

3

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.

Langevin answered 23/11, 2012 at 18:58 Comment(2)
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.Chloras
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.Arrio
A
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

Algy answered 23/11, 2012 at 19:13 Comment(2)
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.Conias
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 correspondinglyAlgy
F
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).

Furcate answered 23/11, 2012 at 19:14 Comment(6)
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).Langevin
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.Langevin
@Langevin 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.Furcate
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.Disloyalty
To avoid the "two views being instantiated" problem, you can use: return nibContents[0]; in initWithFrame to use the view loaded from Nib only.Disloyalty
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.Junkojunkyard
L
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];
}
Lacilacie answered 23/11, 2012 at 20:36 Comment(1)
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.Langevin

© 2022 - 2024 — McMap. All rights reserved.