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

发布于 2025-01-05 18:29:09 字数 120 浏览 6 评论 0原文

我想在故事板中的多个视图控制器中使用视图。因此,我考虑在外部 xib 中设计视图,以便更改反映在每个视图控制器中。但是如何从故事板中的外部 xib 加载视图?这是否可能?如果不是这种情况,还有哪些其他替代方案可以满足上述情况?

I want to use a view throughout multiple viewcontrollers in a storyboard. Thus, I thought about designing the view in an external xib so changes are reflected in every viewcontroller. But how can one load a view from a external xib in a storyboard and is it even possible? If thats not the case, what other alternatives are availble to suit the situation abouve?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

锦欢 2025-01-12 18:29:09

我的完整示例位于此处,但我将在下面提供摘要。

布局

将同名的 .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 并将类名称设置为您的自定义类名称。

输入图像描述这里

My full example is here, but I will provide a summary below.

Layout

Add a .swift and .xib file each with the same name to your project. The .xib file contains your custom view layout (using auto layout constraints preferably).

Make the swift file the xib file's owner.

enter image description here
Code

Add the following code to the .swift file and hook up the outlets and actions from the .xib file.

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

Use it

Use your custom view anywhere in your storyboard. Just add a UIView and set the class name to your custom class name.

enter image description here

世俗缘 2025-01-12 18:29:09

有一段时间,Christopher Swasey 的方法是我发现的最好方法。我向团队中的几位高级开发人员询问了这个问题,其中一位拥有完美的解决方案!它满足了 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. 像平常使用助理编辑器一样连接您的插座。
    • 注意:如果您查看Connections Inspector,您会发现您的引用插座没有引用您的自定义类(即MyCustomClass),而是引用文件的所有者。由于 File's Owner 被指定为您的自定义类,因此插座将连接并正常工作。
  6. 确保您的自定义类在类语句之前有@IBDesignable。
  7. 使您的自定义类符合下面引用的 NibLoadable 协议。
    • 注意:如果您的自定义类 .swift 文件名与 .xib 文件名不同,请将 nibName 属性设置为.xib 文件的名称。
  8. 实现必需的 init?(coder aDecoder: NSCoder)覆盖 init(frame: CGRect) 以调用 setupFromNib(),如下例所示。
  9. 将 UIView 添加到所需的 Storyboard 并将类设置为您的自定义类名称(即 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()
    }

}

注意:如果您错过了陷阱并将 .xib 文件中的 class 值设置为自定义类,那么它将不会在情节提要中绘制,并且您将收到 EXC_BAD_ACCESS 错误当你运行应用程序,因为它陷入了尝试使用 init?(coder aDecoder: NSCoder) 方法从 nib 初始化类的无限循环,然后调用 Self.nib.instantiate< /code> 并再次调用 init

For a while Christopher Swasey's approach was the best approach I had found. I asked a couple of the senior devs on my team about it and one of them had the perfect solution! It satisfies every one of the concerns that Christopher Swasey so eloquently addressed and it doesn't require boilerplate subclass code(my main concern with his approach). There is one gotcha, but other than that it is fairly intuitive and easy to implement.

  1. Create a custom UIView class in a .swift file to control your xib. i.e. MyCustomClass.swift
  2. Create a .xib file and style it as you want. i.e. MyCustomClass.xib
  3. Set the File's Owner of the .xib file to be your custom class (MyCustomClass)
  4. GOTCHA: leave the class value (under the identity Inspector) for your custom view in the .xib file blank. So your custom view will have no specified class, but it will have a specified File's Owner.
  5. Hook up your outlets as you normally would using the Assistant Editor.
    • NOTE: If you look at the Connections Inspector you will notice that your Referencing Outlets do not reference your custom class (i.e. MyCustomClass), but rather reference File's Owner. Since File's Owner is specified to be your custom class, the outlets will hook up and work propery.
  6. Make sure your custom class has @IBDesignable before the class statement.
  7. Make your custom class conform to the NibLoadable protocol referenced below.
    • NOTE: If your custom class .swift file name is different from your .xib file name, then set the nibName property to be the name of your .xib file.
  8. Implement required init?(coder aDecoder: NSCoder) and override init(frame: CGRect) to call setupFromNib() like the example below.
  9. Add a UIView to your desired storyboard and set the class to be your custom class name (i.e. MyCustomClass).
  10. Watch IBDesignable in action as it draws your .xib in the storyboard with all of it's awe and wonder.

Here is the protocol you will want to reference:

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

And here is an example of MyCustomClass that implements the protocol (with the .xib file being named 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()
    }

}

NOTE: If you miss the Gotcha and set the class value inside your .xib file to be your custom class, then it will not draw in the storyboard and you will get a EXC_BAD_ACCESS error when you run the app because it gets stuck in an infinite loop of trying to initialize the class from the nib using the init?(coder aDecoder: NSCoder) method which then calls Self.nib.instantiate and calls the init again.

东北女汉子 2025-01-12 18:29:09

假设您已经创建了一个要使用的 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(右上角的第三个图标,看起来像一个带有线条的矩形),然后在“自定义类”下的“类”中输入子类的名称。

Assuming that you've created an xib that you want to use:

1) Create a custom subclass of UIView (you can go to File -> New -> File... -> Cocoa Touch Class. Make sure "Subclass of:" is "UIView").

2) Add a view that's based on the xib as a subview to this view at initialization.

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

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

In 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) Wherever you want to use it in your storyboard, add a UIView as you normally would, select the newly added view, go to the Identity Inspector (the third icon on the upper right that looks like a rectangle with lines in it), and enter your subclass's name in as the "Class" under "Custom Class".

诗笺 2025-01-12 18:29:09

我一直发现“将其添加为子视图”解决方案并不令人满意,因为它与 (1) 自动布局、(2) @IBInspectable 和 (3) 出口相混淆。相反,让我向您介绍 awakeAfter: 的魔力,这是一个 NSObject 方法。

awakeAfter 允许您将实际从 NIB/Storyboard 唤醒的对象完全替换为不同的对象。然后,该对象将经历水合过程,调用 awakeFromNib,将其添加为视图等。

我们可以在“纸板剪纸”中使用它我们视图的子类,其唯一目的是从 NIB 加载视图并将其返回以在 Storyboard 中使用。然后,在 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
  }
}

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

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

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

instantiateViewFromNibUIView 的类型安全扩展。它所做的只是循环遍历 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
  }
}

I've always found the "add it as a subview" solution unsatisfactory, seeing as it screws with (1) autolayout, (2) @IBInspectable, and (3) outlets. Instead, let me introduce you to the magic of awakeAfter:, an NSObject method.

awakeAfter lets you swap out the object actually woken up from a NIB/Storyboard with a different object entirely. That object is then put through the hydration process, has awakeFromNib called on it, is added as a view, etc.

We can use this in a "cardboard cut-out" subclass of our view, the only purpose of which will be to load the view from the NIB and return it for use in the Storyboard. The embeddable subclass is then specified in the Storyboard view's identity inspector, rather than the original class. It doesn't actually have to be a subclass in order for this to work, but making it a subclass is what allows IB to see any IBInspectable/IBOutlet properties.

This extra boilerplate might seem suboptimal—and in a sense it is, because ideally UIStoryboard would handle this seamlessly—but it has the advantage of leaving the original NIB and UIView subclass completely unmodified. The role it plays is basically that of an adapter or bridge class, and is perfectly valid, design-wise, as an additional class, even if it is regrettable. On the flip side, if you prefer to be parsimonious with your classes, @BenPatch's solution works by implementing a protocol with some other minor changes. The question of which solution is better boils down to a matter of programmer style: whether one prefers object composition or multiple inheritance.

Note: the class set on the view in the NIB file remains the same. The embeddable subclass is only used in the storyboard. The subclass can't be used to instantiate the view in code, so it shouldn't have any additional logic, itself. It should only contain the awakeAfter hook.

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

⚠️ The one significant drawback here is that if you define width, height, or aspect ratio constraints in the storyboard that don't relate to another view then they have to be copied over manually. Constraints that relate two views are installed on the nearest common ancestor, and views are woken from the storyboard from the inside-out, so by the time those constraints are hydrated on the superview the swap has already occurred. Constraints that only involve the view in question are installed directly on that view, and thus get tossed when the swap occurs unless they are copied.

Note that what is happening here is constraints installed on the view in the storyboard are copied to the newly instantiated view, which may already have constraints of its own, defined in its nib file. Those are unaffected.

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 is a type-safe extension to UIView. All it does is loop through the NIB's objects until it finds one that matches the type. Note that the generic type is the return value, so the type has to be specified at the call site.

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
  }
}
铜锣湾横着走 2025-01-12 18:29:09

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

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

有一种优雅的方法可以在不使用文件所有者的情况下完成此操作。请查看此博客文章。它解释了如何以正确的方式进行操作。

Although the top most popular answers works fine, they are conceptually wrong. They all use File's owner as connection between class's outlets and UI components. File's owner is supposed to be used only for top-level objects not UIViews. Check out Apple developer document.
Having UIView as File's owner leads to these undesirable consequences.

  1. You are forced to use contentView where you are supposed to use self. It’s not only ugly, but also structurally wrong because the intermediate view keeps data structure from conveying it’s UI structure. It's like going against the concept of declarative UI.
  2. You can only have one UIView per Xib. An Xib is supposed to have multiple UIViews.

There's elegant way to do it without using File's owner. Please check this blog post. It explains how to do it the right way.

简单气质女生网名 2025-01-12 18:29:09

我认为使用 XIB 视图的替代方案是在单独的故事板中使用视图控制器。

然后在主情节提要中代替自定义视图,使用容器视图Embed Segue,并使用StoryboardReference到这个自定义视图控制器 哪个视图应放置在主情节提要中的其他视图内。

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

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

I think about alternative for using XIB views to be using View Controller in separate storyboard.

Then in main storyboard in place of custom view use container view with Embed Segue and have StoryboardReference to this custom view controller which view should be placed inside other view in main storyboard.

Then we can set up delegation and communication between this embed ViewController and main view controller through prepare for segue. This approach is different then displaying UIView, but much simpler and more efficiently (from programming perspective) can be utilised to achieve the same goal, i.e. have reusable custom view that is visible in main storyboard

The additional advantage is that you can implement you logic in CustomViewController class and there set up all delegation and view preparation without creating separate (harder to find in project) controller classes, and without placing boilerplate code in main UIViewController using Component. I think this is good for reusable components ex. Music Player component (widget like) that is embeddable in other views.

不知在何时 2025-01-12 18:29:09

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

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

Best solution currently is to just use a custom view controller with its view defined in a xib, and simply delete the "view" property that Xcode creates inside the storyboard when adding the view controller to it (don't forget to set the name of the custom class though).

This will make the runtime automatically look for the xib and load it. You can use this trick for any kind of container views, or content view.

萌面超妹 2025-01-12 18:29:09

根据 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.mMyView.xib

首先准备您的 MyView.xib,正如 Ben Patch 的回复 所说,所以 set class MyView< /code> 表示文件的所有者,而不是此 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];

警告! 如果满足以下条件,则该视图的预览将不会显示在 Storyboard 中:由于 Xcode >= 9.2 中的此错误,您使用 WatchKit 扩展: https://forums.developer.apple.com/thread/95616

Solution for Objective-C according to steps described in Ben Patch's response.

Use extension for 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

Create files MyView.h, MyView.m and MyView.xib.

First prepare your MyView.xib as Ben Patch's response says so set class MyView for File's owner instead of main view inside this 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

And later just create your view programatically:

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

Warning! Preview of this view will not be shown in Storyboard if you use WatchKit Extension because of this bug in Xcode >= 9.2: https://forums.developer.apple.com/thread/95616

欢你一世 2025-01-12 18:29:09

这就是您一直想要的答案。您只需创建您的 CustomView 类,将其主实例与所有子视图和出口一起放在 xib 中。然后,您可以将该类应用于故事板或其他 xib 中的任何实例。

无需摆弄文件的所有者,或将出口连接到代理或以特殊方式修改 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

汤姆

Here's the answer you've wanted all along. You can just create your CustomView class, have the master instance of it in a xib with all the subviews and outlets. Then you can apply that class to any instances in your storyboards or other xibs.

No need to fiddle with File's Owner, or connect outlets to a proxy or modify the xib in a peculiar way, or add an instance of your custom view as a subview of itself.

Just do this:

  1. Import BFWControls framework
  2. Change your superclass from UIView to NibView (or from UITableViewCell to NibTableViewCell)

That's it!

It even works with IBDesignable to refer your custom view (including the subviews from the xib) at design time in the storyboard.

You can read more about it here:
https://medium.com/build-an-app-like-lego/embed-a-xib-in-a-storyboard-953edf274155

And you can get the open source BFWControls framework here:
https://github.com/BareFeetWare/BFWControls

And here's a simple extract of the NibReplaceable code that drives it, in case you're curious:
https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4

Tom ????

月亮邮递员 2025-01-12 18:29:09

即使您的类与 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];
       }
}

This solution can be used even if your class does not have the same name as the XIB.
For example, if you have a base view controller class controllerA which has a XIB name controllerA.xib and you subclassed this with controllerB and want to create an instance of controllerB in a storyboard, then you can:

  • create the view controller in the storyboard
  • set the class of the controller to the controllerB
  • delete the view of the controllerB in the storyboard
  • override load view in controllerA to:

*

- (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];
       }
}
念﹏祤嫣 2025-01-12 18:29:09

类 BYTXIBView: UIView {
var nibView: UIView?

// MARK: - init methods
override init(frame: CGRect) {
    super.init(frame: frame)
    commonSetup()
}

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

func commonSetup() {
    guard let nibView = loadViewFromNib() else { return }
    nibView.frame = bounds
    nibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    addSubview(nibView)
}

func loadViewFromNib() -> UIView? {
    let bundle = Bundle(for: type(of: self))
    let className = type(of: self).className
    let view = bundle.loadNibNamed(className, owner: self, options: nil)?.first as? UIView
    return view
}

}

class BYTXIBView: UIView {
var nibView: UIView?

// MARK: - init methods
override init(frame: CGRect) {
    super.init(frame: frame)
    commonSetup()
}

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

func commonSetup() {
    guard let nibView = loadViewFromNib() else { return }
    nibView.frame = bounds
    nibView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    addSubview(nibView)
}

func loadViewFromNib() -> UIView? {
    let bundle = Bundle(for: type(of: self))
    let className = type(of: self).className
    let view = bundle.loadNibNamed(className, owner: self, options: nil)?.first as? UIView
    return view
}

}

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文