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

泪湿孤枕 提交于 2019-12-02 14:03:20

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:

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

This is your xib:

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

and this is the final result:

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.

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

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()
}

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]

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.

smileBot
  • Create xib file File > New > New File > iOS > User Interface > View
  • Create custom UIView class File > New > New File > iOS > Source > CocoaTouch
  • Assign the xib file's identity to the custom view class
  • In viewDidLoad of the view controller initialize the xib and its associated file using loadNibNamed: on NSBundle.mainBundle and the first view returned can be added as a subview of self.view.
  • The custom view loaded from the nib can be saved to a property for setting the frame in viewDidLayoutSubviews. Just set the frame to self.view's frame unless you need to make it smaller than self.view.

    class ViewController: UIViewController {
    
        weak var customView: MyView!
    
        override func viewDidLoad() {
            super.viewDidLoad()
            self.customView = NSBundle.mainBundle().loadNibNamed("MyView", owner: self, options: nil)[0] as! MyView
            self.view.addSubview(customView)
            addButtonHandlerForCustomView()
        }
    
        private func addButtonHandlerForCustomView() {
            customView.buttonHandler = {
                [weak self] (sender:UIButton) in
                guard let welf = self else {
                    return
                }
                welf.buttonTapped(sender)
            }
        }
    
        override func viewDidLayoutSubviews() {
            self.customView.frame = self.view.frame
        }
    
        private func buttonTapped(button:UIButton) {
    
        }
    }
    
  • Also, if you want to talk back from the xib to your UIViewController instance then create a weak property on the custom view's class.

    class MyView: UIView {
    
        var buttonHandler:((sender:UIButton)->())!
    
        @IBAction func buttonTapped(sender: UIButton) {
            buttonHandler(sender:sender)
        }
    }
    

Here's the project on GitHub

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

A little bit more swifty version of @brandonscript 's idea with early return:

override func awakeFromNib() {

    guard let xibName = xibName,
          let xib = Bundle.main.loadNibNamed(xibName, owner: self, options: nil),
          let views = xib as? [UIView] else {
        return
    }

    if views.count > 0 {
        self.addSubview(views[0])
    }

}

While I don't recommend the path you're going down it can be done by placing an "embedded view controller view" where you want the view to appear.

Embed a view controller that contains a single view -- the view you want to be reused.

It's been a while on this one, and I've seen a number of answers go by. I recently revisited it because I had just been using UIViewController embedding. Which works, until you want to put something in "an element repeated at runtime" (e.g. a UICollectionViewCell or a UITableViewCell). The link provided by @TomSwift led me to follow the pattern of

A) Rather than make the parent view class be the custom class type, make the FileOwner be the target class (in my example, CycleControlsBar)

B) Any outlet/action linking of the nested widgets goes to that

C) Implement this simple method on CycleControlsBar:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    if let container = (Bundle.main.loadNibNamed("CycleControlsBar", owner: self, options: nil) as? [UIView])?.first {
        self.addSubview(container)
        container.constrainToSuperview() // left as an excise for the student
    }
}

Works like a charm and so much simpler than the other approaches.

Here's the answer you've wanted all along. You can just create your CustomView class, have the master instance of it in a xib with all the subviews and outlets. Then you can apply that class to any instances in your storyboards or other xibs.

No need to fiddle with File's Owner, or connect outlets to a proxy or modify the xib in a peculiar way, or add an instance of your custom view as a subview of itself.

Just do this:

  1. Import BFWControls framework
  2. Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell)

That's it!

It even works with IBDesignable to render your custom view (including the subviews from the xib) at design time in the storyboard.

You can read more about it here: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

And you can get the open source BFWControls framework here: https://github.com/BareFeetWare/BFWControls

And here's a simple extract of the code that drives it, in case you're curious: https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

Tom 👣

The "correct" answer is that you are not meant to make re-usable views with corresponding nibs. If a view subclass is valuable as a reusable object it rarely will need a nib to go with it. Take for example every view subclass provided by UIKit. Part of this thinking is a view subclass that is actually valuable wont be implemented using a nib, which is the general view at Apple.

Usually when you use a view in nib or storyboard you will want to tweak it graphically for the given use case anyway.

You might consider using "copy paste" for recreating same or similar views instead of making separate nibs. I this believe accomplishes the same requirements and it will keep you more or less in line with what Apple is doing.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!