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

时间:2015-01-06 22:01:06

标签: ios objective-c xcode interface-builder uistoryboard

我在XCode / iOS世界中相对较新;我已经完成了一些不错的基于故事板的应用程序,但我从来没有在整个nib / xib上扯掉我的东西。我想使用相同的工具为场景设计/布局可重用的视图/控件。所以我为我的视图子类创建了我的第一个xib并绘制了它:

enter image description here

我连接了我的插座并设置了约束,就像我以前在故事板中所做的一样。我将File Owner的类设置为我的自定义UIView子类的类。所以我假设我可以用一些API实例化这个视图子类,它将如图所示配置/连接。

现在回到我的故事板中,我想嵌入/重用它。我在表视图原型单元格中这样做:

enter image description here

我有一个观点。我已将它的类设置为我的子类。我已经为它创建了一个插座,所以我可以操纵它。

64美元的问题是在哪里/如何表明仅仅将我的视图子类的空/未配置实例放在那里是不够的,而是使用我创建的.xib来配置/实例化它?这真的很酷,如果在XCode6中,我可以输入XIB文件用于给定的UIView,但是我没有看到用于执行该操作的字段,因此我假设我必须在代码中执行某些操作。

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

更新

我能够通过实现我的表格单元格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;
}

这是否像它那样容易?或者我为此工作太辛苦了?

11 个答案:

答案 0 :(得分:26)

你几乎就在那里。您需要在分配视图的自定义类中覆盖initWithCoder。

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

一旦完成,StoryBoard就会知道将xib加载到UIView中。

这里有一个更详细的解释:

这就是你的UIViewController在你的故事板上的样子: enter image description here

蓝色空间基本上是一个UIView,它将"持有"你的xib。

这是你的xib:

enter image description here

有一个Action连接到其上的按钮,将打印一些文本。

这是最终结果:

enter image description here

第一次点击和第二次点击之间的区别在于第一次点击是使用UIViewController添加到StoryBoard。第二个是使用代码添加的。

答案 1 :(得分:16)

您需要在自定义awakeAfterUsingCoder:子类中实现UIView。此方法允许您将解码的对象(来自故事板)与不同的对象(来自可重用的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;
}

有一篇非常好的文章here讨论了这种技术和一些替代方案。

此外,如果您在@interface声明中添加IB_DESIGNABLE并提供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;
}

答案 2 :(得分:15)

这种Interface Builder和Swift 4的非常酷且可重用的方式:

  1. 像这样创建一个新类:

    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. 在您的故事板中,添加一个UIView,它将充当Xib的容器。给它一个类名XibView

  3. 在此新XibView的属性检查器中,在IBInspectable字段中设置.xib的名称(不带文件扩展名):

  4. 将新的Xib视图添加到项目中,并在属性检查器中设置Xib"文件的所有者"到XibView(确保您只将#34;文件的所有者"设置为自定义类,不要将内容视图子类化,否则会崩溃),再次,设置IBInspectable字段:

  5. 有一点需要注意:这假设您将.xib框架与其容器匹配。如果您没有,或者需要它可以调整大小,您需要添加一些编程约束或修改子视图的框架以适应。我使用来简化:

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

    加分

    据称,您可以使用prepareForInterfaceBuilder()在Interface Builder中显示这些可重复使用的视图,但我没有太多运气。 This blog建议添加contentView属性,并调用以下内容:

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

答案 3 :(得分:2)

您只需将UIView拖放到您的IB中并将其插入并设置

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

enter image description here

步骤

  1. 添加新文件=>用户界面=> UIView
  2. 设置自定义类 - yourUIViewClass
  3. 设置恢复ID - yourUIViewClass
  4. yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject]; [self.view addSubview:yourView]
  5. 现在您可以根据需要自定义视图。

答案 4 :(得分:2)

  • 创建xib文件 File > New > New File > iOS > User Interface > View
  • 创建自定义UIView类 File > New > New File > iOS > Source > CocoaTouch
  • 将xib文件的标识分配给自定义视图类
  • 在视图控制器的viewDidLoad中,使用loadNibNamed:上的NSBundle.mainBundle初始化xib及其关联文件,并且可以将返回的第一个视图添加为self.view的子视图。
  • 可以将从笔尖加载的自定义视图保存到属性,以便在viewDidLayoutSubviews中设置框架。只需将框架设置为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) {
    
        }
    }
    
  • 此外,如果您想从xib与UIViewController实例进行对话,请在自定义视图的类上创建一个弱属性。

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

以下是GitHub

上的项目

答案 5 :(得分:2)

我多年来一直在使用此代码段。如果您计划在XIB中使用自定义类视图,请将其放在自定义类的.m文件中。

作为副作用,它会导致调用awakeFromNib,因此您可以将所有初始化/设置代码留在那里。

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

答案 6 :(得分:1)

早期回归@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])
    }

}

答案 7 :(得分:0)

虽然我不建议您沿路径走,但可以通过在要显示该视图的位置放置一个“嵌入式视图控制器视图”来完成。

嵌入包含单个视图的视图控制器-您要重用的视图。

答案 8 :(得分:0)

这已经有一段时间了,我已经看到了很多答案。我最近重新访问了它,因为我刚刚使用过UIViewController嵌入。在您希望将某些内容放入“在运行时重复的元素”之前(例如,UICollectionViewCell或UITableViewCell),该方法才有效。 @TomSwift提供的链接使我遵循了

的模式

A)而不是使父视图类成为自定义类类型,而是使FileOwner作为目标类(在我的示例中为CycleControlsBar)

B)嵌套窗口小部件的所有出口/动作链接都将到达该位置

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

像魅力一样工作,并且比其他方法简单得多。

答案 9 :(得分:0)

这是您一直想要的答案。您可以只创建CustomView类,并将其主实例包含在带有所有子视图和出口的xib中。然后,您可以将该类应用于情节提要或其他xib中的任何实例。

无需摆弄File的所有者,或将插座连接到代理,也无需以特殊方式修改xib,或将自定义视图的实例添加为其自身的子视图。

只需执行以下操作:

  1. 导入BFWControls框​​架
  2. 将您的超类从UIView更改为NibView(或从UITableViewCell更改为NibTableViewCell

就是这样!

它甚至可以与IBDesignable一起在设计时在情节提要中呈现您的自定义视图(包括xib的子视图)。

您可以在此处了解更多信息: https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

您可以在此处获得开源的BFWControls框​​架: https://github.com/BareFeetWare/BFWControls

以下是驱动它的代码的简单摘录,以防您好奇: https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

汤姆

答案 10 :(得分:-2)

"正确"答案是,您并不打算使用相应的笔尖制作可重复使用的视图。如果视图子类作为可重用对象很有价值,那么它很少需要一个笔尖来使用它。以UIKit提供的每个视图子类为例。这种想法的一部分是一个视图子类,它实际上很有价值,不会使用nib实现,这是Apple的一般视图。

通常当您在笔尖或故事板中使用视图时,无论如何都需要以图形方式对其进行调整。

您可以考虑使用"复制粘贴"用于重新创建相同或类似的视图,而不是制作单独的笔尖。我相信它可以达到相同的要求,它会让你或多或少地与苹果公司的行为保持一致。