21

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?)

Forge
  • 6,538
  • 6
  • 44
  • 64
ThomasM
  • 2,647
  • 3
  • 25
  • 30

6 Answers6

26

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;
}
Yang Meyer
  • 5,409
  • 5
  • 39
  • 51
  • 1
    sounds great except when using ARC you can't assign to self outside of an init method :-( – Ben Clayton Sep 05 '12 at 10:06
  • 4
    You are right about ARC. See my followup post at http://blog.yangmeyer.de/blog/2012/07/09/an-update-on-nested-nib-loading/ – Yang Meyer Sep 11 '12 at 09:39
  • 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. – DTs Jan 03 '13 at 00:06
  • 1
    @Yang Meyer the links in your answer as well the blogpost are not working anymore. Can you please update those? – Nick Weaver Nov 09 '15 at 12:27
6

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.

Eimantas
  • 48,927
  • 17
  • 132
  • 168
  • 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.. – ThomasM Mar 09 '11 at 14:09
  • 1
    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. – Eimantas Mar 09 '11 at 14:15
  • 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? – ThomasM Mar 09 '11 at 14:20
  • 1
    You'd place an action in MainController and connect IBAction outlet from button to file owner. – Eimantas Mar 09 '11 at 14:22
  • 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? – ThomasM Mar 09 '11 at 14:28
  • I think this breaks MVC. The view becomes responsible not only for presentation, but performing actions as well (which usually is controller's responsibility). – Eimantas Mar 09 '11 at 14:42
  • 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." – ThomasM Mar 09 '11 at 14:45
  • 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. – Eimantas Mar 09 '11 at 14:54
  • 1
    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? – ThomasM Mar 09 '11 at 14:59
4

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.

user4951
  • 32,206
  • 53
  • 172
  • 282
  • We found it simpler to build the NIB the normal way and instantiate it the same as this answer: http://stackoverflow.com/questions/9014105/where-is-a-good-tutorial-for-making-a-custom-uitableviewcell – Rembrandt Q. Einstein Jun 12 '13 at 20:13
2

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)]];
Leszek Szary
  • 9,763
  • 4
  • 55
  • 62
1

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.)

Rembrandt Q. Einstein
  • 1,101
  • 10
  • 23
-1

Instead of subclassing UIView why don't you subclass UIViewController. Check out the following link. In that made a "RedView" and "BlueView" UIViewControllers with their xibs and added them to the MultipleViewsController view by creating and instance of the former two classes and adding [self.view addSubview:red.view] and [self.view addSubview:blue.view] in the MultipleViewsController's viewDidLoad method

MultipleControllers in one view

Just add (id)sender to the button pressed function in RedView and BlueView in the code of the above link.

Community
  • 1
  • 1
HG's
  • 818
  • 6
  • 22
  • 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 Mar 09 '11 at 13:57
  • 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. – HG's Mar 09 '11 at 14:15
  • 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." http://developer.apple.com/library/ios/#featuredarticles/ViewControllerPGforiPhoneOS/AboutViewControllers/AboutViewControllers.html#//apple_ref/doc/uid/TP40007457-CH112-SW10 – ThomasM Mar 09 '11 at 14:25
  • Ohk thanks for pointed that out. I didn't know it ain't supported by apple :). It works though – HG's Mar 09 '11 at 14:32