14

I have a UIViewController with some controllers and some views. Two of these views (Grid Cell) are other nibs. I've got outlets from the Grid Cells to File's Owner, but they aren't loaded automatically.

So I try to override GridCell.m's initWithCoder. This starts an infinite loop.

I know it's possible to just override initWithFrame and add the subview from code, but this is not what I want. I want to be able to move the view around in Interface Builder and have Xcode initialize the view with the right frame.

How do I go about achieving this?

EDIT 1

I'm trying to get it working with the help of Alexander. This is how I've now got it set up: MainView has UIView with a Custom class set as GridCell. It got an outlet in the MainView/File's Owner.

Pic 1

Removed all init-code from GridCell.m and set up an outlet to my custom class

Pic 2

Pic 3

The MainView don't still display the GridCell though. There's no error, just a lonely, empty space where the red switch should be. What am I doing wrong?

I'm very close to just doing this programmatically. I would love to learn how to this with nibs though.

Iulian Onofrei
  • 9,188
  • 10
  • 67
  • 113
Jon Ramvi
  • 714
  • 1
  • 6
  • 19
  • `[NSBundle loadNibNamed]` returns a `BOOL`! – trojanfoe Jul 12 '12 at 10:55
  • 2
    Thanks for your reply. NSBundle loadNibNamed returnes an array which I get the first object of: [Apple developer](https://developer.apple.com/library/ios/#documentation/UIKit/Reference/NSBundle_UIKitAdditions/Introduction/Introduction.html) – Jon Ramvi Jul 12 '12 at 10:57
  • 1
    Ah good point - I was looking at the OS X reference :) – trojanfoe Jul 12 '12 at 10:57

5 Answers5

45

Loading the nib causes initWithCoder to be called again, so you only want to do so if the subclass currently doesn't have any subviews.

-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        if (self.subviews.count == 0) {
            UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil];
            UIView *subview = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
            subview.frame = self.bounds;
            [self addSubview:subview];
        }
    }
    return self;
}
Caleb Shay
  • 2,531
  • 1
  • 19
  • 13
  • 1
    This method is consistent with ARC -- and also the only answer that remembers to set subview.frame = self.bounds. – tw airball Apr 28 '13 at 05:14
  • 2
    This works, but it creates an extra `UIView` under your custom view. – Jason Moore Jul 18 '14 at 13:24
  • Incredible. I have been looking for an answer like this since I started iOS a couple of weeks ago. You have saved me. – clocksmith Aug 23 '14 at 16:15
  • This will result in subview not having any auto layout constraints with self, so if self is resized, the subview will not be automatically. – Siegfoult Apr 01 '15 at 20:33
  • that concept just makes an infinite loop at the moment... and crashes. – holex Jun 17 '15 at 14:09
  • No, this will not work with auto layout. However, you'll note my answer was from 2012, well before auto layout was a thing. Feel free to give an updated answer that works with auto layout. – Caleb Shay Jun 17 '15 at 21:22
  • @CalebShay, I get the error `this class is not key value coding-compliant for the key myLabel`. Any idea why? – Iulian Onofrei Aug 31 '15 at 09:02
  • @IulianOnofrei check your broken connections at Interface Builder – slxl Feb 29 '16 at 15:09
  • It's brilliant. However, if the subclassed view has set up a delegate, this method doesn't work because it's added as a subview. I haven't found a way to do it with nibs and have just resolved to programmatically init the subclassed view. – eager to learn Mar 29 '16 at 22:44
11

Loading a nib will cause the corresponding owner in a

  -(id) initWithCoder:(NSCoder *) coder;  

call

Therefore your coude in this method:

self = [[[NSBundle mainBundle] loadNibNamed: @"GridCell"
owner: self
options: nil] objectAtIndex:0];

will cause again a call of the initWithCoder method. That's because you try to load the nib again. If you define a custom UIView and create a nib file to lay out its subviews you can't just add a UIView to another nib file, change the class name in IB to your custom class and expect the nib loading system to figure it out. What you could do is the following:

Your custom view's nib file needs to have the 'File's owner' class set to your custom view class and you need to have an outlet in your custom class called 'toplevelSubView' connected to a view in your custom view nib file that is acting as a container for all the subviews. Add additional outlets to your view class and connect up the subviews to 'File's owner' (your custom UIView). (See https://stackoverflow.com/a/7589220/925622)

EDIT Okay, to answer your edited question I would do the following:

Go to the nib file where you want to include the custom view with it's nib file layouting it. Do not make it to the custom view (GridCell) itself, instead make a view which will contain your grid cell (gridCellContainer for example, but it should be a UIView) Customize the initWithFrame method within your custom view like you did in initWithCoder:

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"GridCell" owner:self options:nil];    
        self = [nib objectAtIndex:0];
        self.frame = frame;
    }
    return self;
}

And then, in the viewController which is the fileOwner for the view where you want to include your custom view (the one with the gridCellContainer view) do this in viewDidLoad e.g.

//...
GridCell *gridCell = [[GridCell alloc] initWithFrame:self.viewGridCellContainer.bounds];
[self.viewGridCellContainer addSubview:gridCell];

Now eveything should work as you expected

Community
  • 1
  • 1
Alexander
  • 7,178
  • 8
  • 45
  • 75
  • Thanks for your reply. In your link, Robin seems to be loading a nib in initWithCoder as me; but this is what causes the infinite loop - how is this a problem for me but a solution for him? I'm updating my question with your solution as I'm still having some trouble. I can add pics and stuff there.. – Jon Ramvi Jul 12 '12 at 12:04
  • Updated my question. If you have a minute, please take a look. – Jon Ramvi Jul 12 '12 at 12:25
2

The File's owner will not get a call to

  -(id) initWithCoder:(NSCoder *) coder;  

when loading a xib.

However, every view defined in that xib will get a call to

-(id) initWithCoder:(NSCoder *) coder;  

when loading a xib.

If you have a subclass of a UIView (i.e. GridCell) defined in a xib and also try to load that same xib in your subclass's initWithCoder, you will end up with an infinite loop. However, I can't see what will the use case be.

Usually you design your UIView's subclass (i.e. GridCell) in one xib, then use that subclass in a view controller’s xib for example.

Also, can't see a use case where your custom view will have a subview in it's initWithCoder, i.e.

-(id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        if (self.subviews.count == 0) {
            UINib *nib = [UINib nibWithNibName:NSStringFromClass([self class]) bundle:nil];
            UIView *subview = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
            subview.frame = self.bounds;
            [self addSubview:subview];
        }
    }
    return self;
}

Unless you want to be able to override its view hierarchy on demand in some other xib. Which IMO assumes an external dependency (i.e. a hierarchy defined in another xib) and kinda defeats the purpose of having a reusable UIView in the first place.

Bare in mind that when loading a xib, passing an instance as the File's owner, will have all of its IBOutlet(s) set. In that case, you would be replacing self (i.e. GridCell) with whatever the root view in that GridCell.xib is, losing all IBOutlet connections in the process.

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        NSArray *nib = [[NSBundle mainBundle] loadNibNamed:@"GridCell" owner:self options:nil];    
        self = [nib objectAtIndex:0];
        self.frame = frame;
    }
    return self;
}

There is a more detailed post on "How to implement a reusable UIView." that goes into a bit more detail as well and hopefully clears things up.

qnoid
  • 2,346
  • 2
  • 26
  • 45
1

loadNibNamed:: will call initWithCoder:

Why don't you follow this pattern?

-(id)initWithCoder:(NSCoder *)coder
{

    if (self = [super initWithcoder:coder]) {

       // do stuff here ... 

    }

    return self;                 
}

Does [super initWithcoder:coder] do things that you want to avoid?

Hermann Klecker
  • 14,039
  • 5
  • 48
  • 71
0

I had the same issue when I'm trying to override initWithsomething method, we need

-(id)initWithsomething:(Something *)something
{
    if (self = [super initWithsomething:something]) {
       // do stuff here ... 
    }

    return self; 
}

instead

-(id)initWithsomething:(Something *)something
{
    if (self = [super init]) {
       // do stuff here ... 
    }

    return self;
}
Emil Marashliev
  • 238
  • 2
  • 12
  • This isn't really a question, but you're far off, so I wanted to help you out. super refers to the class you're extending: when the header file starts with MyCustomLabel : UILabel, super is the UIlabel. That's why, when you initialize your own object, you also need to initialize the super with [super init]. You should never use initWithsomething in MyCustomLabel AND call super's initWithsomething, because you're then overriding the super's method with your own, since it's the same name. That is, your method should not be called initWithCoder nor initWithFrame. Come up with something new – Jon Ramvi Nov 14 '12 at 13:54