在情节串连板中从外部 xib 文件加载视图

我想在故事板中使用一个贯穿多个视图控制器的视图。因此,我考虑在一个外部的 xib 中设计视图,以便每个视图控制器都能反映变化。但是如何从故事板中的外部 xib 加载视图呢?如果情况并非如此,还有什么其他选择可以适应上述情况?

138591 次浏览

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

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

即使类的名称与 XIB 不同,也可以使用此解决方案。 例如,如果你有一个基础视图控制器类控制器 A,它有一个 XIB 名称控制器 A.XIB,你将它与控制器 B 子类化,并想在故事板中创建一个控制器 B 的实例,那么你可以:

  • 在故事板中创建视图控制器
  • 将控制器的类设置为控制器 B
  • 删除故事板中控制器 B 的视图
  • 将控制器 A 中的加载视图覆盖到:

*

- (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];
}
}

我的完整示例是 给你,但我将在下面提供一个摘要。

布局

添加。迅速和。每个 xib 文件与您的项目具有相同的名称。那个。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

假设您已经创建了一个希望使用的 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,选择新添加的视图,进入身份检查器(右上角的第三个图标,看起来像一个矩形,其中有线条) ,并在“自定义类”下输入子类的名称为“类”。

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

awakeAfter允许您将实际从 NIB/Storyboard 唤醒的对象与完全不同的对象交换出来。然后将 那个物体放入水合过程中,有 awakeFromNib调用的,作为视图添加等。

我们可以在视图的“硬纸板剪切”子类中使用它,它的唯一用途是从 NIB 加载视图并返回它以在情节串联板中使用。然后在 Storyboard 视图的标识检查器中指定可嵌入的子类,而不是原始类。它实际上不必是一个子类才能工作,但是使它成为一个子类可以让 IB 看到任何 IBChectable/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
}
}

这里一个显著的缺点是,如果你在故事板中定义宽度、高度或者长宽比限制,而这些限制与另一个视图无关,那么它们必须被手动复制。关联两个视图的约束被安装在最近的共同祖先上,视图从故事板中由内而外地被唤醒,所以当这些约束在监控视图上得到充分利用时,交换已经发生了。只涉及有问题的视图的约束直接安装在该视图上,因此在交换发生时抛出,除非复制它们。

注意,这里发生的情况是,视图 在故事板里上安装的约束被复制到 新实例化的视图新实例化的视图可能已经有了自己的约束,定义在它的 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
}
}

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

有一段时间,Christopher Swasey 的方法是我找到的最好的方法。我问了我团队里的几个高级开发人员,其中一个有 完美的解决方案!它满足了 Christopher Swasey 雄辩地解决的每一个问题,并且不需要样板子类代码(我对他的方法的主要关注)。有 抓到你了,但除此之外,它是相当直观和容易实现。

  1. 在一个. swift 文件中创建一个自定义 UIView 类来控制您的 xib
  2. 创建一个. xib 文件并按照您的需要设置它的样式
  3. 将. xib 文件的 File's Owner设置为自定义类(MyCustomClass)
  4. 中的自定义视图保留 class值(在 identity Inspector下)。XIB 档案空白。< em > 因此您的自定义视图将没有指定的类,但是它将有一个指定的 File’s Owner。
  5. 连接您的插座,因为您通常会使用 Assistant Editor
    • 注意: 如果你查看 Connections Inspector,你会注意到你的引用插座不引用你的自定义类(即 MyCustomClass) ,而是引用 File's Owner。因为 File's Owner被指定为您的自定义类,所以插座将连接并工作属性。
  6. 确保您的自定义类在 class 语句之前具有@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 并在。Xib 文件是你的自定义类,然后它不会在故事板中绘制,当你运行应用程序时,你会得到一个 EXC_BAD_ACCESS错误,因为它陷入了一个无限循环,试图使用 init?(coder aDecoder: NSCoder)方法从 nib 初始化类,然后调用 Self.nib.instantiate并再次调用 init

我认为 alternative使用 XIB views是使用 View Controller 在不同的情节串连图板中

然后在主故事板中代替自定义视图使用 container viewEmbed Segue,并且对这个 自定义视图控制器使用 StoryboardReference,这个视图应该放置在主故事板中的其他视图中。

然后通过 准备继续实现 建立代表团和嵌入式 ViewController 与主视图控制器之间的通信。这种方法是 与众不同然后显示 UIView,但更简单和更有效(从编程的角度来看)可以用来实现相同的目标,即有可重用的自定义视图,可见于主故事板

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

根据 本 · 帕奇的回应中描述的步骤解决 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,因为 本 · 帕奇的回应说,这样设置类 MyView为文件的所有者,而不是主视图内的这个 XIB。

返回文章页面

#import <UIKit/UIKit.h>


IB_DESIGNABLE @interface MyView : UIView


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


@end

返回文章页面

#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];

警告!由于 Xcode 中的这个 bug,如果你使用 WatchKit 扩展,这个视图的预览将不会显示在 Storyboard 中。 = 9.2: < a href = “ https://forums.developer.apple.com/thread/95616”rel = “ nofollow norefrer”> https://forums.developer.apple.com/thread/95616

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

没有必要摆弄文件的所有者,或连接插座到代理或修改 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

汤姆

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

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

有一种不使用 File's owner的优雅方法。请检查这个 博客文章。它解释了如何用正确的方法来做。

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

}