UIView subclass with its own XIB [duplicate]

I created a custom UIView subclass, and would prefer to not layout the UI in code in the UIView subclass. I'd like to use a xib for that. So what I did is the following.

I created a class "ShareView" which subclasses UIView. I created a XIB file with its file's owner set to "ShareView". Then I link some outlets I declared in my "ShareView.h".

Next I have a ViewController, MainViewController, which adds the ShareView as a subview. whith this code:

NSArray *arr = [[NSBundle mainBundle] loadNibNamed:@"ShareView" owner:nil options:nil];
UIView *fv = [[arr objectAtIndex:0] retain];
fv.frame = CGRectMake(0, 0, 320, 407);
[self.view addSubview:fv];

But now I get NSUnknownKeyException errors on the outlets I declared in my ShareView.

The reason I did all this is because I want a UIView, with its own logic in a seperate XIB file. I read in several places that ViewControllers are only used to manage a full screen, i.e. not parts of a screen... So what am I doing wrong? I want my logic for ShareView in a seperate class, so my MainController class doesn't get bloated with logic from ShareView (which I think is an aption to solve this problem?)


ThomasM,

We had similar ideas about encapsulating behavior inside a custom view (say, a slider with companion labels for min/max/current values, with value-changed events also handled by the control internally).

In our current best-practice, we would design the ShareView in Interface Builder (ShareView.xib), as described by Eimantas in his answer. We then embed the ShareView to the view hierarchy in MainViewController.xib.

I wrote up how we embed custom-view Nibs inside other Nibs in our iOS developer blog. The crux is overriding -awakeAfterUsingCoder: in your custom view, replacing the object loaded from MainViewController.xib with the one loaded from the "embedded" Nib (ShareView.xib).

Something along these lines:

// ShareView.m
- (id) awakeAfterUsingCoder:(NSCoder*)aDecoder {
    BOOL theThingThatGotLoadedWasJustAPlaceholder = ([[self subviews] count] == 0);
    if (theThingThatGotLoadedWasJustAPlaceholder) {
        // load the embedded view from its Nib
        ShareView* theRealThing = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([ShareView class]) owner:nil options:nil] objectAtIndex:0];

        // pass properties through
        theRealThing.frame = self.frame;
        theRealThing.autoresizingMask = self.autoresizingMask;

        [self release];
        self = [theRealThing retain];
    }
    return self;
}

You defined owner of the loaded xib as nil. Since file owner in xib itself has outlets connected and is defined as instance of ShareView you get the exception about unknown keys (nil doesn't have outleted properties you defined for ShareView).

You should define the loader of the xib as owner (i.e. view controller responsible for loading the xib). Then add separate UIView object to xib and define it as instance of ShareView. Then when loading the xib.

ShareView *shareView = [[[[NSBundle mainBundle] loadNibNamed:@"ShareView" owner:self options:nil] objectAtIndex:0] retain];

You can also define shareView as an IBOutlet in view controller's interface (and connect the outlet from file owner to that view in the xib itself). Then when you load the xib there won't be any need for reassigning the shareView instance variable since the xib loading process will reconnect the view to the instance variable directly.


I would like to add to the answer. I hope people would improve this answer though.

First of all it DOES work.

XIB:

enter image description here

Result:

enter image description here

I would like to subclass UIView for a long time especially for tableViewCell.

This is how I did it.

It's succesful, but some part is still "awkward" in my opinion.

First I created a usual .h, .m, and xib file. Notice that Apple do not have the check box to automatically create an xib if the subclass you created is not a subclass of UIViewController. Well create those anyway.

#import <UIKit/UIKit.h>
#import "Business.h"

@interface BGUIBusinessCellForDisplay : UITableViewCell

+ (NSString *) reuseIdentifier;


- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz;
@end

Really simple UITableViewCell, that I want to initialize latter with biz.

I put reuseidentifier which you should do for UITableViewCell

//#import "Business.h"
@interface BGUIBusinessCellForDisplay ()
@property (weak, nonatomic) IBOutlet UILabel *Title;
@property (weak, nonatomic) IBOutlet UIImageView *Image;
@property (weak, nonatomic) IBOutlet UILabel *Address;
@property (weak, nonatomic) IBOutlet UILabel *DistanceLabel;
@property (weak, nonatomic) IBOutlet UILabel *PinNumber;
@property (strong, nonatomic) IBOutlet BGUIBusinessCellForDisplay *view;

@property (weak, nonatomic) IBOutlet UIImageView *ArrowDirection;
@property (weak, nonatomic) Business * biz;
@end

@implementation BGUIBusinessCellForDisplay

- (NSString *) reuseIdentifier {
    return [[self class] reuseIdentifier];
};
+ (NSString *) reuseIdentifier {
    return NSStringFromClass([self class]);
};

Then I eliminated most init codes and put this instead:

- (BGUIBusinessCellForDisplay *) initWithBiz: (Business *) biz
{
    if (self.biz == nil) //First time set up
    {
        self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
        NSString * className = NSStringFromClass([self class]);
        //PO (className);
        [[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
        [self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
    }
    if (biz==nil)
    {
        return self;
    }

    self.biz = biz;
    self.Title.text = biz.Title; //Let's set this one thing first
    self.Address.text=biz.ShortenedAddress;

    //if([self.distance isNotEmpty]){
    self.DistanceLabel.text=[NSString stringWithFormat:@"%dm",[biz.Distance intValue]];
    self.PinNumber.text =biz.StringPinLineAndNumber;

Notice that it's really awkward.

First of all the init can be used in 2 ways.

  1. It can be used to right after aloc
  2. It can be used by we having another existing class and then we just want to init that existing cell to another biz.

So I did:

if (self.biz == nil) //First time set up
{
    self = [super init]; //If use dequeueReusableCellWithIdentifier then I shouldn't change the address self points to right
    NSString * className = NSStringFromClass([self class]);
    //PO (className);
    [[NSBundle mainBundle] loadNibNamed:className owner:self options:nil];
    [self addSubview:self.view]; //What is this for? self.view is of type BGCRBusinessForDisplay2. That view should be self, not one of it's subview Things don't work without it though
}

Another icky things that I did is when I do [self addSubview:self.view];

The thing is I want self to be the view. Not self.view. Somehow it works nevertheless. So yea, please help me improve, but that's essentially the way to implement your own subclass of UIView.


You can create your custom UIView designed in xib and even make Interface Builder to display it inside other xib files or storyboards in new Xcode 6 using IB_DESIGNABLE. In xib set file owner to your custom class but do not set UIView class to avoid recurrency loading problems. Just leave default UIView class and you will add this UIView as a subview of your custom class view. Connect all your outlets to file owner and in your custom class load your xib like in the code below. You can check my video tutorial here: https://www.youtube.com/watch?v=L97MdpaF3Xg

IB_DESIGNABLE
@interface CustomControl : UIView
@end

@implementation CustomControl

- (instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super initWithCoder:aDecoder])
    {
        [self load];
    }
    return self;
}

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame])
    {
        [self load];
    }
    return self;
}

- (void)load
{
    UIView *view = [[[NSBundle bundleForClass:[self class]] loadNibNamed:@"CustomControl" owner:self options:nil] firstObject];
    [self addSubview:view];
    view.frame = self.bounds;
}

@end

If you are using autolayout then you might want to change: view.frame = self.bounds; to:

[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];
[self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[view]|" options:0 metrics:nil views:NSDictionaryOfVariableBindings(view)]];