UIView subclass with its own XIB

22,402

Solution 1

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;
}

Solution 2

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.

Solution 3

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.

Solution 4

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)]];

Solution 5

To use Yang's pattern with Auto-Layout, you need to add the following somewhere in the -awakeWithCoder: method.

    theRealThing.translatesAutoresizingMaskIntoConstraints = NO;

If you don't turn off -translatesAutoResizingMaskIntoConstraints it can cause your layout to be incorrect as well as causing a LOT of debugging nonsense in the console.

EDIT: Auto-layout can still be a pain. Certain constraints aren't respected, but other are (e.g. pinning to the bottom doesn't work but pinning to the top does). We're not exactly sure why, but you can work around this by manually passing constraints from the placeholder to theRealThing.

It's also worth noting that this pattern works just the same way with Storyboards as it does with regular .xibs (i.e. you can create a UI Element in a .xib and drop it into a StoryBoard View controller by following your steps.)

Share:
22,402
ThomasM
Author by

ThomasM

Front-end developer (HTML, CSS, Javascript)

Updated on July 09, 2022

Comments

  • ThomasM
    ThomasM almost 2 years

    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
    ThomasM about 13 years
    Like I said in the question: "I read in several places that ViewControllers are only used to manage a full screen, i.e. not parts of a screen..."
  • ThomasM
    ThomasM about 13 years
    I did what you said (partially) and it still doesn't work.. I say partially, because I don't understand what you exactly mean with: "Then add separate UIView object to xib and define it as instance of ShareView." I am now getting NSUnknownException errors because "MainController" doesn't implement the outlets in ShareView. Which is why I don't get your answer. Why should "MainController" be the owner of shareView when shareView's logic is located in ShareView.m, and ShareViews class identity in Interface Builder is set to ShareView? Hope that makes sense somehow..
  • Eimantas
    Eimantas about 13 years
    Here's how to create a xib: 1) create a view xib from File -> New dialog window. 2) Define MainController as file owner. 3) Select view object in the xib and set it as subclass of ShareView. 4) Double click that view and edit it to your heart's content. 5) Connect outlets from that view object to the subviews you added.
  • HG's
    HG's about 13 years
    In your question, you are creating a view with the xib and adding it to your main view through the addSubview method. In the link i gave you, i made two controllers and added redView and blueView to the same view. Both are controlled by their own controllers. You can add their logic in their own classes. Also, you can use the Interface builder to modify both the views.
  • ThomasM
    ThomasM about 13 years
    Suppose I take your approach, and I place a button in the view in ShareView.xib, where would I place the -(IBAction) tappedMyButton {}... Does it go in the ShareView.m file?
  • Eimantas
    Eimantas about 13 years
    You'd place an action in MainController and connect IBAction outlet from button to file owner.
  • ThomasM
    ThomasM about 13 years
    Ah, yes I see now. But that method seems to not be supported by apple. If you look it up in the documentation it says the following: "You should not use multiple custom view controllers to manage different portions of the same view hierarchy. Similarly, you should not use a single custom view controller object to manage multiple screens worth of content." developer.apple.com/library/ios/#featuredarticles/…
  • ThomasM
    ThomasM about 13 years
    That's what I thought... But i DON'T want that. What I want is to be able to put that function in ShareView.m so that all code that is used for outlets in the ShareView is placed in ShareView.m ... Or is that just plain wrong?
  • HG's
    HG's about 13 years
    Ohk thanks for pointed that out. I didn't know it ain't supported by apple :). It works though
  • Eimantas
    Eimantas about 13 years
    I think this breaks MVC. The view becomes responsible not only for presentation, but performing actions as well (which usually is controller's responsibility).
  • ThomasM
    ThomasM about 13 years
    Ok, so is there a way that I can achieve this then, without subclassing UIViewController, as that's not supported by apple. They say so in their documentation "You should not use multiple custom view controllers to manage different portions of the same view hierarchy. Similarly, you should not use a single custom view controller object to manage multiple screens worth of content."
  • Eimantas
    Eimantas about 13 years
    I don't get one thing. If that view must perform same action in every controller it's loaded by - why not implement the IBAction in every view controller that loads aforementioned view? Also - if there is more than one IBAction (more than one button or label or anything) - why not fill the view to the whole screen and make separate view controller for it? If view must perform same action on every view controller - consider setting view's delegate (yet another property) to some object right after loading the xib.
  • ThomasM
    ThomasM about 13 years
    Ofcourse that is an option. But I guess you know as well as I do that duplicate code in the different view controllers is not desirable... What if I want to change somethng in that IBAction?
  • Ben Clayton
    Ben Clayton over 11 years
    sounds great except when using ARC you can't assign to self outside of an init method :-(
  • Yang Meyer
    Yang Meyer over 11 years
    You are right about ARC. See my followup post at blog.yangmeyer.de/blog/2012/07/09/…
  • DTs
    DTs over 11 years
    Something to be aware of when using this technique, is that awakeFromNib will be called twice for theRealThing (that is, for the same instance). Need to account for that if your implementation is overriding it.
  • Rembrandt Q. Einstein
    Rembrandt Q. Einstein almost 11 years
    We found it simpler to build the NIB the normal way and instantiate it the same as this answer: stackoverflow.com/questions/9014105/…
  • Nick Weaver
    Nick Weaver over 8 years
    @Yang Meyer the links in your answer as well the blogpost are not working anymore. Can you please update those?