简体   繁体   English

如何在故事板场景中嵌入自定义视图xib?

[英]How to embed a custom view xib in a storyboard scene?

I'm relatively new in the XCode/iOS world; 我在XCode / iOS世界中相对较新; I've done some decent sized storyboard based apps, but I didn't ever cut me teeth on the whole nib/xib thing. 我已经完成了一些不错的基于故事板的应用程序,但我从来没有在整个nib / xib上扯掉我的东西。 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: 所以我为我的视图子类创建了我的第一个xib并绘制了它:

在此输入图像描述

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. 我将我的File Owner的类设置为我的自定义UIView子类的类。 So I assume I can instantiate this view subclass with some API, and it will configured/connected as shown. 所以我假设我可以用一些API实例化这个视图子类,它将如图所示配置/连接。

Now back in my storyboard, I want to embed/reuse this. 现在回到我的故事板中,我想嵌入/重用它。 I'm doing so in a table view prototype cell: 我在表视图原型单元格中这样做:

在此输入图像描述

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? 64美元的问题是在哪里/如何表明仅仅放置我的视图子类的空/未配置实例是不够的,但是使用我创建的.xib来配置/实例化它? 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. 这真的很酷,如果在XCode6中,我可以输入XIB文件用于给定的UIView,但我没有看到这样做的字段,所以我假设我必须在代码中的某些地方做一些事情。

(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) (我确实在SO上看到了这样的其他问题,但是没有找到任何关于这部分难题的问题,或者最新的XCode6 / 2015)

Update 更新

I am able to get this to kind of work by implementing my table cell's awakeFromNib as follows: 我可以通过实现我的表格单元格的awakeFromNib来实现这一点,如下所示:

- (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? 还是我为此努力工作?

You're almost there. 你快到了。 You need to override initWithCoder in your custom class you assigned the view to. 您需要在分配视图的自定义类中覆盖initWithCoder。

- (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. 一旦完成,StoryBoard将知道在UIView中加载xib。

Here's a more detailed explanation: 这是一个更详细的解释:

This is how your UIViewController looks like on your story board: 这就是你的故事板上你的UIViewController样子: 在此输入图像描述

The blue space is basically a UIView that will "hold" your xib. 蓝色空间基本上是一个将“保持”你的xib的UIView。

This is your xib: 这是你的xib:

在此输入图像描述

There's an Action connected to a button on it that will print some text. 有一个Action连接到它上面的一个按钮,它将打印一些文本。

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 . 第一个clickMe和第二个之间的区别在于第一个是使用StoryBoard添加到UIViewController的。 The second was added using code. 第二个是使用代码添加的。

You need to implement awakeAfterUsingCoder: in your custom UIView subclass. 您需要在自定义UIView子类中实现awakeAfterUsingCoder: . This method allows you to exchange the decoded object (from the storyboard) with a different object (from your reusable xib), like so: 此方法允许您将解码的对象(来自故事板)与不同的对象(来自可重用的xib)交换,如下所示:

- (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声明中,并提供initWithFrame:方法,则可以在IB中使用设计时预览(需要Xcode 6!):

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: 这个Interface Builder和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. 在您的故事板中,添加一个UIView,它将充当Xib的容器。 Give it a class name of XibView : 给它一个XibView的类名:

  3. In the property inspector of this new XibView , set the name of your .xib (without the file extension) in the IBInspectable field: 在此新XibView的属性检查器中,在IBInspectable字段中设置XibView的名称(不带文件扩展名):

  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: 将新的Xib视图添加到项目中,并在属性检查器中,将Xib的“文件所有者”设置为XibView (确保您只将“文件所有者”设置为自定义类,不要将内容视图子类化,或者它将崩溃),并再次设置IBInspectable字段:

One thing to note: This assumes that you're matching the .xib frame to its container. 有一点需要注意:这假设您将.xib框架与其容器匹配。 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. 据称你可以使用prepareForInterfaceBuilder()在Interface Builder中看到这些可重用的视图,但我没有太多运气。 This blog suggests adding a contentView property, and calling the following: 此博客建议添加contentView属性,并调用以下内容:

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

You just have to drag and drop UIView in your IB and outlet it and set 你只需要在你的IB中拖放UIView并将其插入并设置即可

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

在此输入图像描述

Step

  1. Add New File => User Interface => UIView 添加新文件=>用户界面=> UIView
  2. Set Custom Class - yourUIViewClass 设置自定义类 - yourUIViewClass
  3. Set Restoration ID - yourUIViewClass 设置恢复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. 现在,您可以根据需要自定义视图。

  • Create xib file File > New > New File > iOS > User Interface > View 创建xib文件File > New > New File > iOS > User Interface > View
  • Create custom UIView class File > New > New File > iOS > Source > CocoaTouch 创建自定义UIView类File > New > New File > iOS > Source > CocoaTouch
  • Assign the xib file's identity to the custom view class 将xib文件的标识分配给自定义视图类
  • 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. 在视图控制器的viewDidLoad中,使用loadNibNamed:NSBundle.mainBundle上初始化xib及其关联文件,并且返回的第一个视图可以添加为loadNibNamed:的子视图。
  • The custom view loaded from the nib can be saved to a property for setting the frame in viewDidLayoutSubviews . 从笔尖加载的自定义视图可以保存到属性中,以便在viewDidLayoutSubviews设置框架。 Just set the frame to self.view 's frame unless you need to make it smaller than self.view . 只需将框架设置为self.view的框架,除非你需要使它小于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. 此外,如果要从xib与UIViewController实例进行对话,请在自定义视图的类上创建弱属性。

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

Here's the project on GitHub 这是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. 如果您计划在XIB中使用自定义类视图,请将其放在自定义类的.m文件中。

As a side effect it results in awakeFromNib being called so you can leave all your init/setup code in there. 作为副作用,它会导致调用awakeFromNib,因此您可以将所有init / setup代码保留在那里。

- (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: 早期回归@brandonscript的想法更加流畅一点:

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. 我最近重新访问它,因为我刚刚使用UIViewController嵌入。 Which works, until you want to put something in "an element repeated at runtime" (eg a UICollectionViewCell or a UITableViewCell). 这有效,直到你想在“运行时重复的元素”(例如UICollectionViewCell或UITableViewCell)中添加一些东西。 The link provided by @TomSwift led me to follow the pattern of @TomSwift提供的链接让我遵循了这种模式

A) Rather than make the parent view class be the custom class type, make the FileOwner be the target class (in my example, CycleControlsBar) A)不要让父视图类成为自定义类类型,而是让FileOwner成为目标类(在我的例子中,CycleControlsBar)

B) Any outlet/action linking of the nested widgets goes to that B)嵌套小部件的任何插座/动作链接都是这样的

C) Implement this simple method on CycleControlsBar: C)在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. 您可以创建CustomView类,在xib中包含所有子视图和出口的主实例。 Then you can apply that class to any instances in your storyboards or other xibs. 然后,您可以将该类应用于故事板或其他xib中的任何实例。

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. 无需摆弄文件所有者,或将插座连接到代理或以特殊方式修改xib,或添加自定义视图的实例作为其自身的子视图。

Just do this: 这样做:

  1. Import BFWControls framework 导入BFWControls框​​架
  2. Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell ) 将您的超类从UIView更改为NibView (或从UITableViewCell更改为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. 它甚至可以与IBDesignable一起在故事板中的设计时渲染自定义视图(包括xib中的子视图)。

You can read more about it here: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155 您可以在此处阅读更多相关信息: 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 你可以在这里获得开源的BFWControls框​​架: 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 这里有一个驱动它的代码的简单摘录,以防你好奇: 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. 以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. 这种想法的一部分是一个视图子类,实际上很有价值,不会使用nib实现,这是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. 我相信它可以达到相同的要求,它会让你或多或少地与苹果公司的做法保持一致。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM