如何在情节提要场景中嵌入自定义视图 xib?

Posted

技术标签:

【中文标题】如何在情节提要场景中嵌入自定义视图 xib?【英文标题】:How to embed a custom view xib in a storyboard scene? 【发布时间】:2015-03-04 16:02:38 【问题描述】:

我是 XCode/ios 领域的新手;我已经完成了一些相当大的基于故事板的应用程序,但我从来没有在整个 nib/xib 事情上咬牙切齿。我想对场景使用相同的工具来设计/布局可重用的视图/控件。所以我为我的视图子类创建了我的第一个 xib 并将其绘制出来:

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

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

我有一个看法。我已经将它的类设置为我的子类。我已经为它创建了一个出口,所以我可以操纵它。

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;

那么这很容易吗?还是我为此付出了太多努力?

【问题讨论】:

1) 你不应该指定文件的所有者,因为如果 xib 只是自定义视图,你就没有文件的所有者。当你加载 nib owner 参数时应该是 nil。 2)您应该为您的视图指定自定义类。并将插座连接到此视图 【参考方案1】:

你快到了。您需要在将视图分配给的自定义类中覆盖 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 就会知道在 UIView 中加载 xib。

这里有更详细的解释:

这就是你的UIViewController 在你的故事板上的样子:

蓝色空间基本上是一个 UIView,它将“容纳”您的 xib。

这是你的 xib:

有一个动作连接到它上面的一个按钮,它会打印一些文本。

这是最终的结果:

第一个clickMe和第二个的区别是第一个是用StoryBoard添加到UIViewController的。第二个是使用代码添加的。

【讨论】:

我不知道它是否会起作用,但你不能在layoutSubviews而不是initWithCoder:中添加视图并创建类IB_DESIGNABLE吗?我想这取决于IB_DESIGNABLE 类是否能够从包中加载 我不知道我是否误用了您的答案,但它没有按预期工作。在一种情况下,我将它放在 .xib 已注册为文件所有者的 ProgramProgressControl 类中。在这种情况下,它在super 发送时引发了异常。另一方面,我将它放置在要放置的表格单元子类中。视图有点出现,但甚至不在外壳内部,而且我似乎没有被连接到插座。 我不确定您的代码是如何工作的,但我为您找到了一个完整的示例,它演示了 initWithCoder andxib 文件的工作原理。 ***.com/questions/9251202/… @TravisGriggs 我已经用更多示例和可以玩的示例项目编辑了我的答案。 或者,您可以覆盖 awakeAfterUsingCoder: 并进行实际的视图交换,而不是覆盖 initWithCoder。相同的工作量,更好的结果。【参考方案2】:

您需要在您的自定义UIView 子类中实现awakeAfterUsingCoder:。此方法允许您将解码的对象(来自情节提要)与不同的对象(来自可重用的 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 讨论了这种技术和一些替代方法。

此外,如果您将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;

【讨论】:

这绝对比“使用第二个包装视图并将您的真实视图添加为 initWithCoder 中的子视图” slapdashery 更可取。任何繁重的上下文移植都可以在类别/扩展方法中完成。删除一些配置的一个建议:大概您在 xib 中有子视图,否则将毫无意义,因此可能会依赖子视图为空或不为空,而不是 tag 属性。 不幸的是,这不可能。【参考方案3】:

界面生成器和 Swift 4 的一种非常酷且可重用的方式:

    像这样创建一个新类:

    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)
        
    
    
    

    在您的故事板中,添加一个 UIView 作为 Xib 的容器。给它一个类名XibView

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

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

需要注意的一点:这假设您将 .xib 框架与其容器相匹配。如果您不这样做,或者需要调整大小,则需要添加一些编程约束或修改子视图的框架以适应。我使用snapkit 让事情变得简单:

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

【讨论】:

非常有趣的解决方案,虽然我收到警告:“IB Designables: Ignoring user defined runtime attribute for key path "xibName" on the instance of "UIView"。尝试设置其时遇到异常value: [ setValue:forUndefinedKey:]: 这个类对于键 xibName 不符合键值编码。” Hrm 看起来你的名字可能有错字?我的第一个猜测是大写 X XibView。 我不这么认为,因为您的解决方案效果很好,我的 XIB 肯定正在加载。如果视图没有正确设置其自定义类,则它根本无法工作。这只是令人讨厌的警告。 哦,很奇怪。如果你弄清楚了,让我知道。 if let views = xib as? [UIView] where views.count > 0 在 swift 语法中更改为 if let views = xib as? [UIView], views.count > 0【参考方案4】:

您只需将 UIView 拖放到您的 IB 中,然后将其导出并设置

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

步骤

    添加新文件 => 用户界面 => UIView 设置自定义类 - yourUIViewClass 设置恢复 ID - yourUIViewClass yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject]; [self.view addSubview:yourView]

现在您可以根据需要自定义视图。

【讨论】:

我必须在viewDidLoad 之前设置插座的值吗?还是之后?还是没关系? viewDidLoad之后:因为如果视图还没有加载,你不能在上面添加另一个子视图。 您在这里展示的是如何添加从 XIB 获取的视图作为 子视图 到我在情节提要中创建的视图和有一个outlet 到。这很酷。但我想要的是用 XIB 视图替换情节提要中的视图。 IOW,我想像占位符一样使用情节提要中的视图。也就是说,我希望应用到情节提要中的视图的属性(例如背景颜色、布局等)仍然适用。【参考方案5】:

多年来,我一直在使用此代码 sn-p。如果您打算在 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】: 创建 xib 文件 File > New > New File > iOS > User Interface > View 创建自定义 UIView 类 File > New > New File > iOS > Source > CocoaTouch 将 xib 文件的标识分配给自定义视图类 在视图控制器的 viewDidLoad 中,使用 loadNibNamed: on NSBundle.mainBundle 初始化 xib 及其关联文件,返回的第一个视图可以添加为 self.view 的子视图。

从 nib 加载的自定义视图可以保存到 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上的项目

【讨论】:

【参考方案7】:

@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])
    


【讨论】:

【参考方案8】:

虽然我不推荐你要走的路径,但可以通过在你希望视图出现的位置放置一个“嵌入式视图控制器视图”来完成。

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

【讨论】:

此方法有效,直到您想将它们放入表格单元格之类的东西中:)【参考方案9】:

这个问题已经有一段时间了,我已经看到了很多答案。我最近重新访问了它,因为我刚刚使用 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
    

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

【讨论】:

【参考方案10】:

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

无需摆弄 File's Owner,或将 outlet 连接到代理或以特殊方式修改 xib,或添加自定义视图的实例作为其自身的子视图。

这样做:

    导入 BFWControls 框架 将您的超类从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

汤姆?

【讨论】:

【参考方案11】:

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

通常,当您在 nib 或情节提要中使用视图时,无论如何您都希望针对给定的用例以图形方式对其进行调整。

您可以考虑使用“复制粘贴”来重新创建相同或相似的视图,而不是制作单独的 nib。我相信这可以满足相同的要求,它会让你或多或少地与 Apple 的做法保持一致。

【讨论】:

我没有听从你的论点。您似乎在说“如果它是可重用的,只需将其全部编码为一个类(布局和所有)”,如果是这样的话,为什么不是所有视图控制器都以这种方式完成,以及任何有用的视图层次结构? 这是一个有趣的观点——可重用视图应该始终在代码中实现,并且 xib 和故事板仅用于从可重用视图组装不可重用视图。这与工具所启用的功能非常一致。但是你还有什么理由说这是“苹果的普遍看法”? 我认为关键是要找到一种方法来做到这一点,尽管苹果公司的普遍看法。这种方法似乎适得其反;重用具有模块化配置的 UIView xib 非常普遍,并且有很多应用程序。 “你为什么要这样做?”对于任何构建一个严肃的应用程序的人来说,这种方法在某些情况下必然不得不违背“苹果正在做的事情”,这种方法是无益的。 (除非您正在为 Apple 自己开发代码) Travis:我的意思是,如果您在 Interface Builder 中设计的视图有用,那么有一种更简单的方法可以复制它。您选择它,按 command-C,然后按 command-V。与将其放置在笔尖中相比,步骤很少。如果您有一个完全在所有地方都相同的视图,并且在很多地方都使用过,并且您希望能够轻松调整而不是有一种新的方法可以做到这一点。但在我开发过的许多应用程序中,并没有出现这种情况。 DivideByZer0:这很有帮助,因为作者试图将他们的工作流程强加到工具上,而不是接受工具。

以上是关于如何在情节提要场景中嵌入自定义视图 xib?的主要内容,如果未能解决你的问题,请参考以下文章

从 Table View Cell xib 与披露指示符转至情节提要

使用自定义 uiview xib

UINavigationBar 自定义颜色与在情节提要中嵌入 UInavigationController

如何从情节提要中的自定义视图制作@IBAction?

如何在 Swift 中将内容包装为自定义视图

如何将子视图添加到情节提要中的自定义 UITableViewCell