从故事板中的外部xib文件加载视图

时间:2012-02-14 18:43:12

标签: ios uiview storyboard xib

我想在故事板中的多个viewcontrollers中使用视图。因此,我考虑在外部xib中设计视图,以便在每个viewcontroller中反映更改。但是如何从故事板中的外部xib加载视图呢?它甚至可能吗?如果不是这样的话,还有哪些其他选择可以适应这种情况呢?

10 个答案:

答案 0 :(得分:99)

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

<强>布局

为项目添加一个名称相同的.swift和.xib文件。 .xib文件包含您的自定义视图布局(最好使用自动布局约束)。

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

enter image description here代码

将以下代码添加到.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并将类名设置为自定义类名。

enter image description here

答案 1 :(得分:51)

有一段时间Christopher Swasey's approach是我找到的最佳方法。我问了我的团队中的几位资深开发员,其中一位有完美的解决方案!它满足了Christopher Swasey如此雄辩地解决的每一个问题,并且它不需要样板子类代码(我主要关注他的方法)。有一个问题,但除此之外,它非常直观且易于实施。

  1. 在.swift文件中创建自定义UIView类以控制xib。即MyCustomClass.swift
  2. 创建.xib文件并根据需要设置样式。即MyCustomClass.xib
  3. 将.xib文件的File's Owner设置为自定义类(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,令人敬畏和惊叹。
  11. 以下是您要参考的协议:

    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并将.xib文件中的class值设置为自定义类,那么它将不会在故事板中绘制,并且当出现EXC_BAD_ACCESS时会出现init?(coder aDecoder: NSCoder)错误你运行应用程序是因为它陷入无限循环,试图使用Self.nib.instantiate方法从笔尖初始化类,然后调用init并再次调用unittest.mock

答案 2 :(得分:29)

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

1)创建UIView的自定义子类(您可以转到File - &gt; New - &gt; File ... - &gt; 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(右上角的第三个图标,看起来像一个带有线条的矩形它),并在“自定义类”下的“类”中输入您的子类名称。

答案 3 :(得分:24)

我总是发现“将其添加为子视图”解决方案令人不满意,因为它与(1)自动布局,(2)@IBInspectable和(3)插座一起使用。相反,让我向您介绍awakeAfter:方法的神奇之处。

NSObject允许您将实际从NIB / Storyboard中唤醒的对象完全替换为另一个对象。 那个对象然后通过水合过程,awakeAfter调用它,被添加为视图等。

我们可以在我们视图的“纸板切出”子类中使用它,其唯一目的是从NIB加载视图并将其返回以在Storyboard中使用。然后在Storyboard视图的标识检查器中指定可嵌入的子类,而不是原始类。它实际上不必是子类以使其工作,但使其成为子类是允许IB查看任何IBInspectable / IBOutlet属性的原因。

注意:NIB文件中视图上设置的类保持不变。可嵌入的子类仅在故事板中使用 。子类不能用于在代码中实例化视图,因此它本身不应该有任何其他逻辑。它应包含awakeFromNib挂钩。

awakeAfter

⚠️这里的一个重要缺点是,如果您在故事板中定义与另一个视图无关的宽度,高度或宽高比约束,则必须手动复制它们。与两个视图相关的约束安装在最近的共同祖先上,并且视图从内到外从故事板中唤醒,因此当超级视图上的这些约束被水合时,交换已经发生。只涉及相关视图的约束直接安装在该视图上,因此在交换发生时会被抛弃,除非它们被复制。

请注意,此处发生的事情是将故事板中的上安装的约束复制到新实例化的视图,该视图可能已经有自己的约束,已定义在它的nib文件中。这些都不受影响。

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

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的类型安全扩展。它所做的只是遍历NIB的对象,直到找到与该类型匹配的对象。请注意,泛型类型是 return 值,因此必须在调用站点指定类型。

UIView

答案 4 :(得分:5)

目前最好的解决方案是使用自定义视图控制器及其在xib中定义的视图,并在添加视图控制器时简单地删除Xcode在故事板中创建的“视图”属性(不要忘记设置虽然自定义类的名称。

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

答案 5 :(得分:5)

我认为alternative使用XIB views在单独的故事板中使用View Controller

然后在主故事板中代替自定义视图,使用container view Embed SegueStoryboardReference到此自定义视图控制器,该视图应放在其他视图中在主要故事板中查看。

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

另外一个优点是你可以在CustomViewController类中实现你的逻辑,并且可以设置所有委托和视图准备,而无需创建单独的(在项目中更难找到)控制器类,也无需使用Component在主UIViewController中放置样板代码。我认为这对可重复使用的组件很有用。可嵌入其他视图的音乐播放器组件(小部件)。

答案 6 :(得分:1)

尽管最流行的答案很有效,但它们是错误的。他们都使用File's owner作为类的出口和UI组件之间的连接,这是非常错误的。 File's owner应该仅用于顶级对象,而不能用于UIView。签出Apple developer document。 将UIView设置为File's owner会导致这些不良后果。

  1. 本应仅使用contentView的情况下,您被迫使用self。这不仅很丑陋,而且在结构上也是错误的,因为数据结构可以更好地传达其UI结构。
  2. 每个Xib只能有一个UIView。 Xib应该具有多个UIView。

有一种不用使用File's owner的优雅方法。请检查此blog post。它说明了如何正确执行操作。

答案 7 :(得分: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];
       }
}

答案 8 :(得分:0)

根据Ben Patch's response中所述步骤的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.mMyView.xib

首先按照Ben Patch's response的说明准备MyView.xib,所以请为文件的所有者(而不是此XIB内部的主视图)设置类MyView

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

答案 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

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

汤姆