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

Seb*_*ann 115 storyboard xib uiview ios

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

Sur*_*gch 116

我的完整示例在这里,但我将在下面提供一个摘要.

布局

添加一个.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
    }
}
Run Code Online (Sandbox Code Playgroud)

用它

在故事板中的任何位置使用自定义视图.只需添加一个UIView并将类名设置为您的自定义类名.

在此输入图像描述

  • 崩溃的另一个常见原因是没有将自定义视图设置为*文件的所有者*.在我的回答中看到红色圆圈. (4认同)
  • 是不是loadNibNamed调用init(编码器:)?我试图调整你的方法崩溃. (3认同)
  • 是的,我设置了根视图的类而不是文件所有者,它导致了无限循环. (3认同)
  • 在 self.addSubview(view) 之前添加 view.autoresizingMask = [.flexibleWidth, .flexibleHeight] 行后,它工作正常。 (2认同)

Ben*_*tch 60

有一段时间克里斯托弗斯瓦西的方法是我找到的最好的方法.我问了我的团队中的几位资深开发员,其中一位有完美的解决方案!它满足了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
    }
}
Run Code Online (Sandbox Code Playgroud)

这是一个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()
    }

}
Run Code Online (Sandbox Code Playgroud)

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

  • 您上面提到的方法非常有效,并且可以在情节提要中立即应用实时预览。绝对方便又棒,功能强大! (4认同)
  • 这是另一个很好的方法,但我觉得上面的方法仍然更好:https://medium.com/zenchef-tech-and-product/how-to-visualize-reusable-xibs-in-storyboards-using- ibdesignable-c0488c7f525d (2认同)

use*_*430 30

假设您已创建了要使用的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;
}
Run Code Online (Sandbox Code Playgroud)

在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)
}
Run Code Online (Sandbox Code Playgroud)

在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)
}
Run Code Online (Sandbox Code Playgroud)

3)无论您想在故事板中使用它,都可以像往常一样添加UIView,选择新添加的视图,转到Identity Inspector(右上角的第三个图标,看起来像一个带有线条的矩形),并在"自定义类"下的"类"中输入子类的名称.

  • 由于无限递归导致崩溃.加载nib会创建子类的另一个实例. (20认同)
  • xib视图的类不应与此新子类相同.如果xib是MyClass,则可以创建这个新类MyClassContainer. (2认同)

Chr*_*sey 26

我总是发现"将它添加为子视图"的解决方案令人不满意,因为它与(1)自动布局,(2)@IBInspectable和(3)插座一起使用.相反,让我向您介绍awakeAfter:一种NSObject方法的魔力.

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

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

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

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

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

请注意,这里发生的事情是故事板中视图上安装的约束被复制到新实例化的视图中,该视图可能已经在其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
  }
}  
Run Code Online (Sandbox Code Playgroud)

UIView是一种类型安全的扩展awakeAfter.它所做的只是遍历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
  }
}
Run Code Online (Sandbox Code Playgroud)

  • 当它被另一个对象替换时,它会制动约束.:( (5认同)

Ing*_*전인건 9

尽管最受欢迎的答案效果很好,但它们在概念上是错误的。它们都File's owner用作类的插座和 UI 组件之间的连接。File's owner应该仅用于顶级对象而不是UIViews。查看Apple 开发人员文档。将 UIView 作为File's owner导致这些不良后果。

  1. 你被迫contentView在你应该使用的地方使用self。它不仅丑陋,而且在结构上也是错误的,因为中间视图阻止数据结构传达其 UI 结构。这就像违背了声明式 UI 的概念。
  2. 每个 Xib 只能有一个 UIView。Xib 应该有多个 UIViews。

有一种优雅的方法可以不使用File's owner. 请查看此博客文章。它解释了如何以正确的方式做到这一点。


Ben*_*n G 5

目前最好的解决方案是使用自定义视图控制器,其视图在xib中定义,只需删除Xcode在添加视图控制器时在故事板中创建的"视图"属性(不要忘记设置名称)虽然自定义类).

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


Mic*_*bro 5

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

然后在主故事板中代替自定义视图使用container view,Embed Segue并且必须StoryboardReference使用此自定义视图控制器,该视图应放置在主故事板中的其他视图内.

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

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