How to embed a custom view xib in a storyboard scene?

41,659

Solution 1

You're almost there. You need to override initWithCoder in your custom class you assigned the view to.

- (id)initWithCoder:(NSCoder *)aDecoder {
    if ((self = [super initWithCoder:aDecoder])) {
        [self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"ViewYouCreated" owner:self options:nil] objectAtIndex:0]];
    }
    return self; }

Once that's done the StoryBoard will know to load the xib inside that UIView.

Here's a more detailed explanation:

This is how your UIViewController looks like on your story board: enter image description here

The blue space is basically a UIView that will "hold" your xib.

This is your xib:

enter image description here

There's an Action connected to a button on it that will print some text.

and this is the final result:

enter image description here

The difference between the first clickMe and the second is that the first was added to the UIViewController using the StoryBoard. The second was added using code.

Solution 2

You need to implement awakeAfterUsingCoder: in your custom UIView subclass. This method allows you to exchange the decoded object (from the storyboard) with a different object (from your reusable xib), like so:

- (id) awakeAfterUsingCoder: (NSCoder *) aDecoder
{
    // without this check you'll end up with a recursive loop - we need to know that we were loaded from our view xib vs the storyboard.
    // set the view tag in the MyView xib to be -999 and anything else in the storyboard.
    if ( self.tag == -999 )
    {
        return self;
    }

    // make sure your custom view is the first object in the nib
    MyView* v = [[[UINib nibWithNibName: @"MyView" bundle: nil] instantiateWithOwner: nil options: nil] firstObject];

    // copy properties forward from the storyboard-decoded object (self)
    v.frame = self.frame;
    v.autoresizingMask = self.autoresizingMask;
    v.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
    v.tag = self.tag;

    // copy any other attribtues you want to set in the storyboard

    // possibly copy any child constraints for width/height

    return v;
}

There's a pretty good writeup here discussing this technique and a few alternatives.

Furthermore, if you add IB_DESIGNABLE to your @interface declaration, and provide an initWithFrame: method you can get design-time preview to work in IB (Xcode 6 required!):

IB_DESIGNABLE @interface MyView : UIView
@end

@implementation MyView

- (id) initWithFrame: (CGRect) frame
{
    self = [[[UINib nibWithNibName: @"MyView"
                            bundle: [NSBundle bundleForClass: [MyView class]]]

             instantiateWithOwner: nil
             options: nil] firstObject];

    self.frame = frame;

    return self;
}

Solution 3

A pretty cool and reusable way of doing this Interface Builder and Swift 4:

  1. Create a new class like so:

    import Foundation
    import UIKit
    
    @IBDesignable class XibView: UIView {
    
        @IBInspectable var xibName: String?
    
        override func awakeFromNib() { 
            guard let name = self.xibName, 
                  let xib = Bundle.main.loadNibNamed(name, owner: self), 
                  let view = xib.first as? UIView else { return }    
            self.addSubview(view)
        }
    
    }
    
  2. In your storyboard, add a UIView that will act as the container for the Xib. Give it a class name of XibView:

  3. In the property inspector of this new XibView, set the name of your .xib (without the file extension) in the IBInspectable field:

  4. Add a new Xib view to your project, and in the property inspector, set the Xib's "File's Owner" to XibView (ensure you've only set the "File's Owner" to your custom class, DO NOT subclass the content view, or it will crash), and again, set the IBInspectable field:

One thing to note: This assumes that you're matching the .xib frame to its container. If you do not, or need it to be resizable, you'll need to add in some programmatic constraints or modify the subview's frame to fit. I use to make things easy:

xibView.snp_makeConstraints(closure: { (make) -> Void in
    make.edges.equalTo(self)
})

Bonus points

Allegedly you can use prepareForInterfaceBuilder() to make these reusable views visible in Interface Builder, but I haven't had much luck. This blog suggests adding a contentView property, and calling the following:

override func prepareForInterfaceBuilder() {
    super.prepareForInterfaceBuilder()
    xibSetup()
    contentView?.prepareForInterfaceBuilder()
}

Solution 4

You just have to drag and drop UIView in your IB and outlet it and set

yourUIViewClass  *yourView =   [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject];
[self.view addSubview:yourView]

enter image description here

Step

  1. Add New File => User Interface => UIView
  2. Set Custom Class - yourUIViewClass
  3. Set Restoration ID - yourUIViewClass
  4. yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject]; [self.view addSubview:yourView]

Now you can customize view as you want.

Solution 5

I've been using this code snippet for years. If you plan on having custom class views in your XIB just drop this in the .m file of your custom class.

As a side effect it results in awakeFromNib being called so you can leave all your init/setup code in there.

- (id)awakeAfterUsingCoder:(NSCoder*)aDecoder {
    if ([[self subviews] count] == 0) {
        UIView *view = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil][0];
        view.frame = self.frame;
        view.autoresizingMask = self.autoresizingMask;
        view.alpha = self.alpha;
        view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
        return view;
    }
    return self;
}
Share:
41,659
Travis Griggs
Author by

Travis Griggs

Updated on March 10, 2020

Comments

  • Travis Griggs
    Travis Griggs about 4 years

    I'm relatively new in the XCode/iOS world; I've done some decent sized storyboard based apps, but I didn't ever cut me teeth on the whole nib/xib thing. I want to use the same tools for scenes to design/layout a reusable view/control. So I created my first ever xib for my view subclass and painted it up:

    enter image description here

    I have my outlets connected and constraints setup, just like I'm used to doing in the storyboard. I set the class of my File Owner to that of my custom UIView subclass. So I assume I can instantiate this view subclass with some API, and it will configured/connected as shown.

    Now back in my storyboard, I want to embed/reuse this. I'm doing so in a table view prototype cell:

    enter image description here

    I've got a view. I've set the class of it to my subclass. I've created an outlet for it so I can manipulate it.

    The $64 question is where/how do I indicate that it's not enough to just put an empty/unconfigured instance of my view subclass there, but to use the .xib I created to configure/instantiate it? It would be really cool, if in XCode6, I could just enter the XIB file to use for a given UIView, but I don't see a field for doing that, so I assume I have to do something in code somewhere.

    (I do see other questions like this on SO, but haven't found any asking for just this part of the puzzle, or up to date with XCode6/2015)

    Update

    I am able to get this to kind of work by implementing my table cell's awakeFromNib as follows:

    - (void)awakeFromNib
    {
        // gather all of the constraints pointing to the uncofigured instance
        NSArray* progressConstraints = [self.contentView.constraints filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(id each, NSDictionary *_) {
            return (((NSLayoutConstraint*)each).firstItem == self.progressControl) || (((NSLayoutConstraint*)each).secondItem == self.progressControl);
        }]];
        // fetch the fleshed out variant
        ProgramProgressControl *fromXIB = [[[NSBundle mainBundle] loadNibNamed:@"ProgramProgressControl" owner:self options:nil] objectAtIndex:0];
        // ape the current placeholder's frame
        fromXIB.frame = self.progressControl.frame;
        // now swap them
        [UIView transitionFromView: self.progressControl toView: fromXIB duration: 0 options: 0 completion: nil];
        // recreate all of the constraints, but for the new guy
        for (NSLayoutConstraint *each in progressConstraints) {
            id firstItem = each.firstItem == self.progressControl ? fromXIB : each.firstItem;
            id secondItem = each.secondItem == self.progressControl ? fromXIB : each.secondItem;
            NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem: firstItem attribute: each.firstAttribute relatedBy: each.relation toItem: secondItem attribute: each.secondAttribute multiplier: each.multiplier constant: each.constant];
            [self.contentView addConstraint: constraint];
        }
        // update our outlet
        self.progressControl = fromXIB;
    }
    

    Is this as easy as it gets then? Or am I working too hard for this?

  • Chris
    Chris over 9 years
    I don't know if it would work, but couldn't you add the view in layoutSubviews instead of initWithCoder: and make the class IB_DESIGNABLE? I guess it would depend on if IB_DESIGNABLE classes are capable of loading from the bundle
  • Travis Griggs
    Travis Griggs over 9 years
    I don't know if I misapplied your answer, but it didn't work as expected. In one case, I placed it in the ProgramProgressControl class that the .xib has registered as a file owner. In that case, it threw an exception at the super send. In the other, I placed it in my table cell subclass, where this is to be placed. The views kind of showed up, but were not even inside of the shell, and I didn't appear to be hooked up to the outlets.
  • Travis Griggs
    Travis Griggs over 9 years
    I'm not following your argument. You seem to be saying "if it's reusable, just code it all up as a class (layout and all)", which if that were the case, why aren't all view controllers done that way, as well as any useful view hierarchy?
  • Segev
    Segev over 9 years
    I'm not sure how your code works but I've found a full example for you that demonstrates how initWithCoder and a xib file works. stackoverflow.com/questions/9251202/…
  • Travis Griggs
    Travis Griggs over 9 years
    Do I have to set the value of the outlet before a viewDidLoad? Or after? Or does it no matter?
  • Yuyutsu
    Yuyutsu over 9 years
    after viewDidLoad: because if view is not loaded yet you can't add another subview on it.
  • Travis Griggs
    Travis Griggs over 9 years
    What you're showing here is how to add a view fetched from an XIB as a subview to a view I created in a storyboard and have an outlet to. Which is cool. But what I wanted is to replace the view in the storyboard with the XIB one. IOW, I want to use the view in the storyboard like a placeholder. That said, I would like the properties applied to the view in the storyboard (things like background color, layout, etc) to still apply.
  • Segev
    Segev over 9 years
    @TravisGriggs I've edited my answer with more examples and an example project you can play with.
  • Travis Griggs
    Travis Griggs over 9 years
    So basicaly, you have to use a 2 view solution. Is that correct? The storyboard has a view that you have an outlet to, and this view has any of the attributes you apply in the storyboard (layout, color, etc). And you then load the XIB instantiated view in as the sole subview of that view. What I was trying to do was swap/merge the "placeholder" view in the storyboard with the one pulled from the XIB. I'm thinking it's not possible, and this double view is the best way to go. Is that correct?
  • Segev
    Segev over 9 years
    Yes. You can't merge two views using the interface builder. Same with swapping. Although our placeholder Custom Class is DoSomething it will have the xib as a subview with a file owner of that same class and that's the "trick". You can, however, replace a UIView with a xib using just code but that defeats the Purpose of this question.
  • TomSwift
    TomSwift over 9 years
    Or, instead of overriding initWithCoder, you can override awakeAfterUsingCoder: and do an actual view swap. Same amount of work, better result.
  • algal
    algal almost 9 years
    This is an interesting perspective — that re-usable views should always be implemented in code, and that xibs and storyboards are only there for assembling non re-usable views from the re-usable views. It is quite consistent with what the tools enable. But do you have any other reason to say this is "the general view at Apple"?
  • Christopher Swasey
    Christopher Swasey over 8 years
    This is definitely preferable to the "use a second wrapping view and add your real view as a subview in initWithCoder" slapdashery. Any heavy context porting can be done in a category/extension method. One suggestion to remove a bit of the configuration: presumably you have subviews in the xib, otherwise it would be pointless, so one might rely on subviews being empty or not instead of the tag property.
  • João Pereira
    João Pereira about 8 years
    Pretty interesting solution, although I'm getting a warning: "IB Designables: Ignoring user defined runtime attribute for key path "xibName" on instance of "UIView". Hit an exception when attempting to set its value: [<UIView 0x7f8ac2f18f30> setValue:forUndefinedKey:]: this class is not key value coding-compliant for the key xibName."
  • brandonscript
    brandonscript about 8 years
    Hrm looks like you maybe have a typo in the name somewhere? My first guess is capital X XibView.
  • João Pereira
    João Pereira about 8 years
    I don't think so because your solution works great, my XIB is definitely being loaded. If the view didn't have its custom class set properly it wouldn't work at all. It's just the warning that's annoying.
  • brandonscript
    brandonscript about 8 years
    Oh weird. If you figure it out lemme know.
  • Fishman
    Fishman over 7 years
    if let views = xib as? [UIView] where views.count > 0 in swift syntax changed to if let views = xib as? [UIView], views.count > 0
  • Fishman
    Fishman over 7 years
    also you can change self.addSubview(xib[0] as! UIView) to self.addSubview(views[0] as! UIView)
  • Hayden Holligan
    Hayden Holligan about 7 years
    A better format for awakeFromNib would be using a guard statement instead of 3 if statements to avoid nesting example override func awakeFromNib() { guard let name = self.xibName, let xib = Bundle.main.loadNibNamed(name, owner: self), let views = xib as? [UIView], views.count > 0 else { return } self.addSubview(views[0] as! UIView) }
  • Hayden Holligan
    Hayden Holligan about 7 years
    Apologies for poor formatting, not sure how the formatting on comments work
  • brandonscript
    brandonscript about 7 years
    Lol they don't. Great addition though! I love guard.
  • DivideByZer0
    DivideByZer0 almost 6 years
    I think the point is to figure out a way to do it, in spite of the general view at Apple. This approach seems counterproductive; reusing UIView xibs that have modular configuration is pretty commonplace and has plenty of applications. The "why would you ever want to do this?" approach is unhelpful for anyone building a serious application that is necessarily going to have to go against "what Apple is doing" in some cases. (unless you're developing code for Apple themselves, that is)
  • Dustin
    Dustin almost 6 years
    Travis: I'm saying if a view you've designed in Interface Builder is useful there is a much easier way to duplicate it. You select it, press command-C, then press command-V. Very few steps compared to locating it in a nib. If you have a view that is exactly the same everywhere, is used in lots of places, and that you want to be able to tweak easily than there is a new way to do that. But in the many many apps I've worked on this just doesn't come up. DivideByZer0: It's helpful in that the author is trying to force their workflow onto the tools instead of embracing the tools.
  • Travis Griggs
    Travis Griggs over 5 years
    This works until you want to put them in things like table cells :)
  • Morten Holmgaard
    Morten Holmgaard over 3 years
    Not possible in swift unfortunately.