从情节提要中的外部Xib文件加载视图


131

我想在情节提要中的多个视图控制器中使用一个视图。因此,我考虑过在外部Xib中设计视图,以使更改反映在每个ViewController中。但是如何从情节提要中的外部xib加载视图,甚至有可能?如果不是这种情况,还有哪些其他替代方法可以适应上述情况?



1
观看此视频:youtube.com/watch?
v=o3MawJVxTgk

Answers:


132

我的完整示例在这里,但是我将在下面提供摘要。

布局

将.swift和.xib文件分别添加到项目中。.xib文件包含您的自定义视图布局(最好使用自动布局约束)。

将swift文件设为xib文件的所有者。

在此处输入图片说明

将以下代码添加到.swift文件,并连接.xib文件中的插座和动作。

import UIKit
class ResuableCustomView: UIView {

    let nibName = "ReusableCustomView"
    var contentView: UIView?

    @IBOutlet weak var label: UILabel!
    @IBAction func buttonTap(_ sender: UIButton) {
        label.text = "Hi"
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)

        guard let view = loadViewFromNib() else { return }
        view.frame = self.bounds
        self.addSubview(view)
        contentView = view
    }

    func loadViewFromNib() -> UIView? {
        let bundle = Bundle(for: type(of: self))
        let nib = UINib(nibName: nibName, bundle: bundle)
        return nib.instantiate(withOwner: self, options: nil).first as? UIView
    }
}

用它

在情节提要中的任何位置使用自定义视图。只需添加,UIView然后将类名设置为您的自定义类名即可。

在此处输入图片说明


3
是loadNibNamed调用init(coder :)吗?我在尝试适应您的方法时遇到了崩溃。
Fishman

@Fishman,如果尝试以编程方式(而不是从情节提要中)加载视图,则该视图将崩溃,因为当前没有init(frame:)。有关更多详细信息,请参见本教程
Suragch

7
崩溃的另一个常见原因不是将自定义视图设置为文件的所有者。看到我回答中的红色圆圈。
Suragch

5
是的,我设置了根视图的类而不是文件所有者,这导致了无限循环。
devios1

2
在self.addSubview(view)之前添加view.autoresizingMask = [.flexibleWidth,.flexibleHeight]行之后,它可以正常工作。
普拉莫·塔帕尼亚

69

一段时间以来,克里斯托弗·斯瓦西(Christopher Swasey)的方法是我发现的最佳方法。我向团队中的几个高级开发人员询问了有关情况,其中有一个完美的解决方案!它满足了Christopher Swasey如此雄辩地解决的所有问题,并且不需要样板代码(我主要关注他的方法)。有一个陷阱,但除此之外,它非常直观且易于实现。

  1. 在.swift文件中创建一个自定义UIView类,以控制xib。即MyCustomClass.swift
  2. 创建一个.xib文件并根据需要设置其样式。即MyCustomClass.xib
  3. File's Owner.xib文件的设置为您的自定义类(MyCustomClass
  4. GOTCHA:将.xib文件中自定义视图的class值(位于下方identity Inspector)留空。因此,您的自定义视图将没有指定的类,但它将具有指定的文件的所有者。
  5. 像平常一样使用来连接网点Assistant Editor
    • 注意:如果您查看,Connections Inspector您会注意到您的引荐出口没有引用您的自定义类(即MyCustomClass),而是引用了File's Owner。由于File's Owner已指定为您的自定义类,因此插座将正常工作。
  6. 确保您的自定义类在类语句之前具有@IBDesignable。
  7. 使您的自定义类符合NibLoadable以下引用的协议。
    • 注意:如果您的自定义类.swift文件名与文件名不同.xib,则将nibName属性设置为文件名.xib
  8. 像下面的示例一样实现required init?(coder aDecoder: NSCoder)override init(frame: CGRect)调用setupFromNib()
  9. 将UIView添加到所需的故事板上,然后将类设置为您的自定义类名称(即MyCustomClass)。
  10. 观看IBDesignable的实际运作,它将您的.xib吸引到情节提要中,这令人敬畏。

这是您要参考的协议:

public protocol NibLoadable {
    static var nibName: String { get }
}

public extension NibLoadable where Self: UIView {

    public static var nibName: String {
        return String(describing: Self.self) // defaults to the name of the class implementing this protocol.
    }

    public static var nib: UINib {
        let bundle = Bundle(for: Self.self)
        return UINib(nibName: Self.nibName, bundle: bundle)
    }

    func setupFromNib() {
        guard let view = Self.nib.instantiate(withOwner: self, options: nil).first as? UIView else { fatalError("Error loading \(self) from nib") }
        addSubview(view)
        view.translatesAutoresizingMaskIntoConstraints = false
        view.leadingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.leadingAnchor, constant: 0).isActive = true
        view.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 0).isActive = true
        view.trailingAnchor.constraint(equalTo: self.safeAreaLayoutGuide.trailingAnchor, constant: 0).isActive = true
        view.bottomAnchor.constraint(equalTo: self.safeAreaLayoutGuide.bottomAnchor, constant: 0).isActive = true
    }
}

这是MyCustomClass实现协议的示例(.xib文件名为MyCustomClass.xib):

@IBDesignable
class MyCustomClass: UIView, NibLoadable {

    @IBOutlet weak var myLabel: UILabel!

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setupFromNib()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupFromNib()
    }

}

注意:如果您错过了Gotcha并将class.xib文件中的值设置为您的自定义类,则它将不会在情节提要中绘制,并且EXC_BAD_ACCESS在运行应用程序时会出现错误,因为它陷入了无限循环中尝试使用该init?(coder aDecoder: NSCoder)方法从笔尖初始化该类,该方法然后调用Self.nib.instantiateinit再次调用。


2
这里是另一个伟大的方式,但我觉得上面的一个犹未:medium.com/zenchef-tech-and-product/...
本补丁

4
您上面提到的方法非常有效,并且可以在情节提要中立即应用实时预览。绝对方便又棒,功能强大!
伊戈尔·列奥诺维奇

1
仅供参考:此解决方案使用中的约束定义setupFromNib(),似乎可以通过自动调整包含XIB创建的视图的表视图单元的大小来解决某些奇怪的自动布局问题。
加里

1
迄今为止最好的解决方案!我喜欢@IBDesignable兼容性。无法相信为什么在添加UIView文件时Xcode或UIKit没有提供像这样的默认值。
fl034

1
似乎无法实现这一目标。每次我在.xib文件中设置文件所有者时,它也会设置自定义类。
drewster

32

假设您已创建要使用的xib:

1)创建一个UIView的自定义子类(您可以转到File-> New-> File ...-> Cocoa Touch Class。确保“ Subclass of:”是“ UIView”)。

2)在初始化时,将基于xib的视图添加为该视图的子视图。

在Obj-C中

-(id)initWithCoder:(NSCoder *)aDecoder{
    if (self = [super initWithCoder:aDecoder]) {
        UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:@"YourXIBFilename"
                                                              owner:self
                                                            options:nil] objectAtIndex:0];
        xibView.frame = self.bounds;
        xibView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
        [self addSubview: xibView];
    }
    return self;
}

在Swift 2

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = NSBundle.mainBundle().loadNibNamed("YourXIBFilename", owner: self, options: nil)[0] as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.FlexibleWidth, .FlexibleHeight]
    self.addSubview(xibView)
}

在Swift 3中

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    let xibView = Bundle.main.loadNibNamed("YourXIBFilename", owner: self, options: nil)!.first as! UIView
    xibView.frame = self.bounds
    xibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    self.addSubview(xibView)
}

3)无论您想在情节提要中使用它的哪个位置,都可以像往常一样添加一个UIView,选择新添加的视图,然后转到Identity Inspector(右上角的第三个图标,看起来像是带有线条的矩形),然后在“自定义类别”下的“类别”中输入子类别的名称。


xibView.frame = self.frame;应该为xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);,否则在将视图添加到情节提要中时,xibView将具有偏移量。
BabyPanda

晚到晚会,但似乎他将其更改为xibView.frame = self.bounds,这是一个没有偏移的帧
Heavy_Bullets

20
由于无限递归导致崩溃。加载笔尖将创建子类的另一个实例。
大卫

2
xib视图的类不应与此新的子类相同。如果xib是MyClass,则可以使这个新类为MyClassContainer。
user1021430

上面的解决方案将崩溃​​到无限递归。
JBarros35

27

我一直发现“添加为子视图”解决方案并不令人满意,因为它带有(1)自动布局,(2)@IBInspectable和(3)插座。相反,让我介绍你的魔法awakeAfter:,一个NSObject方法。

awakeAfter可让您将从NIB / Storyboard唤醒的实际对象完全替换为其他对象。对象,然后通过水合过程的说,已awakeFromNib调用它,加入作为视图等

我们可以在视图的“纸板剪切”子类中使用它,其唯一目的是从NIB加载视图并将其返回以供在Storyboard中使用。然后,在情节提要视图的身份检查器中指定可嵌入的子类,而不是原始类。实际上,它不一定必须是子类才能起作用,但是使它成为子类是使IB能够查看任何IBInspectable / IBOutlet属性的原因。

额外的样板可能看起来不是最佳的,从某种意义上讲,它是理想的,因为理想情况下UIStoryboard可以无缝地进行处理。但是,它的优点是可以使原始的NIB和UIView子类完全保持不变。它扮演的角色基本上是适配器或网桥类的角色,并且在设计上作为附加类是完全有效的,即使很遗憾。另一方面,如果您希望与您的类保持一致,则@BenPatch的解决方案通过实施带有一些其他小的更改的协议来起作用。哪种解决方案更好的问题归结为程序员风格的问题:是更喜欢对象组合还是多重继承。

注意:NIB文件中在视图上设置的类保持不变。可嵌入的子类在情节提要中使用。子类不能用于在代码中实例化视图,因此它本身不应具有任何其他逻辑。它应该包含了awakeAfter钩。

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    return (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)! as Any
  }
}

here️这里的一个重大缺点是,如果在情节提要中定义与另一个视图不相关的宽度,高度或纵横比约束,则必须手动复制它们。关联两个视图的约束安装在最近的公共祖先上,并且视图从内而外从情节提要中唤醒,因此,当这些约束在超级视图上合并时,交换已经发生。仅牵涉到该视图的约束直接安装在该视图上,因此,除非复制它们,否则在发生交换时会被扔掉。

请注意,这里发生的是将安装在情节提要中的视图上安装的约束复制到新实例化的视图,该视图可能已经具有其自身的约束(在其nib文件中定义)。那些不受影响。

class MyCustomEmbeddableView: MyCustomView {
  override func awakeAfter(using aDecoder: NSCoder) -> Any? {
    let newView = (UIView.instantiateViewFromNib("MyCustomView") as MyCustomView?)!

    for constraint in constraints {
      if constraint.secondItem != nil {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: newView, attribute: constraint.secondAttribute, multiplier: constraint.multiplier, constant: constraint.constant))
      } else {
        newView.addConstraint(NSLayoutConstraint(item: newView, attribute: constraint.firstAttribute, relatedBy: constraint.relation, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: constraint.constant))
      }
    }

    return newView as Any
  }
}  

instantiateViewFromNib是的类型安全扩展UIView。它所做的只是遍历NIB的对象,直到找到与该类型匹配的对象为止。请注意,通用类型是返回值,因此必须在调用站点上指定该类型。

extension UIView {
  public class func instantiateViewFromNib<T>(_ nibName: String, inBundle bundle: Bundle = Bundle.main) -> T? {
    if let objects = bundle.loadNibNamed(nibName, owner: nil) {
      for object in objects {
        if let object = object as? T {
          return object
        }
      }
    }

    return nil
  }
}

真是令人难以置信。如果我没记错的话,这只能在“故事板上”起作用-如果您尝试在运行时在代码中创建这样的类,那么我认为它不起作用。我相信。
Fattie

出于所有意图和目的,子类应在代码中与原始类一样工作。如果要用代码从笔尖加载视图,则可以使用相同的技术直接实例化它。子类所做的全部工作就是使用代码从笔尖实例化视图,并将其挂接到情节提要中以供使用。
Christopher Swasey

实际上我错了— 如果您可以实例化它,它也会同样有效,但是您不能这样做,因为NIB中的视图将具有超类作为其类型,因此instantiateViewFromNib不会返回任何内容。无论哪种方式,对于IMO来说都没什么大不了的,子类只是连接到情节提要上的一项便利,所有代码都应位于原始类上。
Christopher Swasey

1
大!一件事让我感到震惊,因为我对xib的经验很少(我只使用情节提要和编程方法),请留在此处以防他人使用:在.xib文件中,您需要选择顶级视图并进行设置其类类型为MyCustomView。在我的xib中,默认情况下缺少左侧的内侧边栏;要打开它,底部/左侧附近的“查看方式:iPhone 7”特征控件旁边有一个按钮。
xaphod

5
当它被另一个对象替换时,它会制动约束。:(
invoodoo

6

当前最好的解决方案是只使用在xib中定义了其视图的自定义视图控制器,并在向其添加视图控制器时简单地删除Xcode在情节提要中创建的“ view”属性(不要忘记设置名称)。自定义类)。

这将使运行时自动查找xib并加载它。您可以将此技巧用于任何类型的容器视图或内容视图。


5

我想alternative利用XIB views被使用View Controller 在不同的故事板

然后在主脚本中代替自定义视图container viewEmbed Segue并且必须StoryboardReference将此定制视图控制器与哪个视图放置在主脚本中的其他视图内。

然后,我们可以通过准备segue在此嵌入式ViewController和主视图控制器之间建立委派和通信。这种方法显示UIView 有所不同,但是(从编程的角度来看)可以更简单,更有效地实现相同的目标,即具有可重复使用的自定义视图,该视图在主故事板上可见

另一个好处是,您可以在CustomViewController类中实现逻辑,并在那里设置所有委派和视图准备,而无需创建单独的(在项目中很难找到)控制器类,也无需使用Component将样板代码放置在主UIViewController中。我认为这对于可重用的组件很有用。可嵌入到其他视图中的Music Player组件(类似Widget)。


确实可悲的是,与使用自定义视图xib相比,解决方案简单得多:(
Wilson

1
在自定义UIView上与Apple的xib生命周期创建链进行交互后,这就是我最终的方法。
LightningStryk

如果您想动态添加自定义视图,我们仍然必须使用单独的视图控制器(最理想的是XIB)
Satyam

5

尽管最流行的答案很好用,但从概念上讲它们是错误的。它们都File's owner用作类出口和UI组件之间的连接。File's owner应该只用于UIViews的顶级对象。查看Apple开发人员文档。使用UIView File's owner会导致这些不良后果。

  1. 您必须contentView在应使用的地方使用self。这不仅丑陋,而且在结构上也是错误的,因为中间视图使数据结构无法传达其UI结构。就像声明性UI的相反。
  2. 每个Xib只能有一个UIView。Xib应该具有多个UIView。

有一种无需使用即可完成的优雅方法File's owner。请检查此博客文章。它说明了如何正确执行操作。


没关系,您也没有解释为什么它不好。我不明白为什么。没有副作用。
JBarros35

1

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

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

只是这样做:

  1. 导入BFWControls框​​架
  2. 将您的超类从更改UIViewNibView(或从更改UITableViewCellNibTableViewCell

而已!

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

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

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

NibReplaceable如果您好奇的话,下面是驱动它的代码的简单摘录:https :
 //gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

汤姆👣


漂亮的解决方案,谢谢!
avee

当本机提供框架时,我不想安装框架。
JBarros35

0

即使您的类与XIB的名称不同,也可以使用此解决方案。例如,如果您有一个基本视图控制器类controllerA,它的XIB名称为controllerA.xib,并且用controllerB对其进行了子类化,并希望在情节提要中创建controllerB的实例,那么您可以:

  • 在情节提要中创建视图控制器
  • 将控制器的类别设置为controllerB
  • 在情节提要中删除controllerB的视图
  • 覆盖controllerA中的负载视图以:

*

- (void) loadView    
{
        //according to the documentation, if a nibName was passed in initWithNibName or
        //this controller was created from a storyboard (and the controller has a view), then nibname will be set
        //else it will be nil
        if (self.nibName)
        {
            //a nib was specified, respect that
            [super loadView];
        }
        else
        {
            //if no nib name, first try a nib which would have the same name as the class
            //if that fails, force to load from the base class nib
            //this is convenient for including a subclass of this controller
            //in a storyboard
            NSString *className = NSStringFromClass([self class]);
            NSString *pathToNIB = [[NSBundle bundleForClass:[self class]] pathForResource: className ofType:@"nib"];
            UINib *nib ;
            if (pathToNIB)
            {
                nib = [UINib nibWithNibName: className bundle: [NSBundle bundleForClass:[self class]]];
            }
            else
            {
                //force to load from nib so that all subclass will have the correct xib
                //this is convenient for including a subclass
                //in a storyboard
                nib = [UINib nibWithNibName: @"baseControllerXIB" bundle:[NSBundle bundleForClass:[self class]]];
            }

            self.view = [[nib instantiateWithOwner:self options:nil] objectAtIndex:0];
       }
}

0

根据Ben Patch响应中所述的步骤为Objective-C解决方案。

对UIView使用扩展名:

@implementation UIView (NibLoadable)

- (UIView*)loadFromNib
{
    UIView *xibView = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];
    xibView.translatesAutoresizingMaskIntoConstraints = NO;
    [self addSubview:xibView];
    [xibView.topAnchor constraintEqualToAnchor:self.topAnchor].active = YES;
    [xibView.bottomAnchor constraintEqualToAnchor:self.bottomAnchor].active = YES;
    [xibView.leftAnchor constraintEqualToAnchor:self.leftAnchor].active = YES;
    [xibView.rightAnchor constraintEqualToAnchor:self.rightAnchor].active = YES;
    return xibView;
}

@end

创建文件MyView.hMyView.m然后MyView.xib

首先MyView.xib按照Ben Patch的回答说,准备您MyView文件,然后为该文件的所有者设置类,而不是在此XIB中设置主视图。

MyView.h

#import <UIKit/UIKit.h>

IB_DESIGNABLE @interface MyView : UIView

@property (nonatomic, weak) IBOutlet UIView* someSubview;

@end

MyView.m

#import "MyView.h"
#import "UIView+NibLoadable.h"

@implementation MyView

#pragma mark - Initializers

- (id)init
{
    self = [super init];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        [self loadFromNib];
        [self internalInit];
    }
    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder
{
    self = [super initWithCoder:aDecoder];
    if (self) {
        [self loadFromNib];
    }
    return self;
}

- (void)awakeFromNib
{
    [super awakeFromNib];
    [self internalInit];
}

- (void)internalInit
{
    // Custom initialization.
}

@end

然后以编程方式创建视图:

MyView* view = [[MyView alloc] init];

警告!如果使用WatchKit Extension,此视图的预览将不会在Storyboard中显示,因为Xcode> = 9.2中存在以下错误:https ://forums.developer.apple.com/thread/95616


应该提供了Swift版本。目前很少有人仍在使用ObjC
萨蒂扬
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.