我希望在Storyboard中的多个视图控制器中使用同一个视图。因此,我考虑在外部XIB中设计视图,以便更改可以反映在每个视图控制器中。但是如何从Storyboard中加载外部XIB中的视图?这是否可行?如果不行,还有哪些其他替代方案可用以适应上述情况?
我希望在Storyboard中的多个视图控制器中使用同一个视图。因此,我考虑在外部XIB中设计视图,以便更改可以反映在每个视图控制器中。但是如何从Storyboard中加载外部XIB中的视图?这是否可行?如果不行,还有哪些其他替代方案可用以适应上述情况?
我的完整示例在这里,但我将在下面提供摘要。
布局
向项目中添加一个同名的.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
并将类名设置为您的自定义类名即可。
有一段时间,Christopher Swasey的方法是我发现的最好的方法。我向我的团队中的几位高级开发人员询问后,其中一位提供了完美的解决方案!它满足了Christopher Swasey所优雅解决的所有问题,并且不需要锅炉板子子类代码(这是我对他的方法的主要担忧)。除此之外,有一个陷阱,但除此之外,它相当直观和易于实现。
MyCustomClass.swift
MyCustomClass.xib
File's Owner
设置为你的自定义类(MyCustomClass
)class
值(在identity Inspector
下)留空。因此,你的自定义视图将没有指定的类,但它将有一个指定的File's Owner。Assistant Editor
一样连接你的输出。Connections Inspector
,你会发现你的引用输出不是引用你的自定义类(例如MyCustomClass
),而是引用了File's Owner
。由于指定了File's Owner
为你的自定义类,因此输出将正确连接并工作。NibLoadable
协议。
.swift
文件名与你的.xib
文件名不同,则将nibName
属性设置为你的.xib
文件名。required init?(coder aDecoder: NSCoder)
和override init(frame: CGRect)
来调用setupFromNib()
,就像下面的示例一样。MyCustomClass
)。下面是你需要参考的协议:
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
值设置为您的自定义类,则它不会在Storyboard中绘制,并且当您运行应用程序时,您会收到一个EXC_BAD_ACCESS
错误,因为它会陷入无限循环,尝试使用init?(coder aDecoder: NSCoder)
方法从nib初始化类,然后调用Self.nib.instantiate
并再次调用init
。
setupFromNib()
中的约束定义,似乎可以修复包含XIB创建视图的自动调整大小表格视图单元格的某些奇怪的自动布局问题。 - Gary@IBDesignable
的兼容性。无法理解为什么在添加 UIView 文件时,Xcode 或 UIKit 没有提供类似于此的默认功能。 - heyfrank假设您已经创建了一个想要使用的xib:
1)创建一个UIView的自定义子类(您可以转到文件->新建->文件...->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) 在storyboard中想要使用的任何位置,按照通常的方式添加一个UIView, 选择新添加的视图,在Identity检查器中(位于右上角的第三个图标,看起来像带有线条的矩形),将你的子类名称作为“Custom Class”下的“Class”输入。
xibView.frame = self.frame;
应该改为 xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
,否则当视图添加到故事板时,xibView 将会有一个偏移量。 - BabyPanda@IBInspectable
和(3)插座。相反,让我向您介绍awakeAfter:
的神奇之处,这是一个NSObject
方法。
awakeAfter
允许您交换从NIB/Storyboard实际唤醒的对象,用完全不同的对象替换它。然后将“那个”对象放入水合过程中,在其上调用awakeFromNib
,将其添加为视图等等。UIStoryboard
将无缝处理这个问题,但它具有将原始NIB和UIView
子类完全保持不变的优点。它扮演的角色基本上是一个适配器或桥接类,并且作为附加类,在设计上是完全有效的,即使有些令人遗憾。另一方面,如果您想节约珍贵的类库,则@BenPatch的解决方案通过实现某些其他次要更改来实现。哪种解决方案更好的问题归结为程序员风格的问题:是否偏爱对象组合还是多重继承。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
}
}
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
}
}
MyCustomView
。在我的xib中,默认情况下缺少左侧内部边栏;要打开它,可以在底部/左侧附近的“查看为:iPhone 7”特性控制旁边找到一个按钮。 - xaphod我在考虑使用独立的storyboard中的View Controller
作为XIB视图的替代方案
。
然后,在主storyboard中,可以使用容器视图
和嵌入式Segue
来代替自定义视图,并且将StoryboardReference
指向这个自定义视图控制器
,该视图应放置在主storyboard的其他视图内。
接下来,我们可以通过准备Segue
来在嵌入的ViewController和主ViewController之间建立委托和通信。这种方法与显示UIView不同,但更简单和更有效(从编程角度)可用于实现相同的目标,即具有在主storyboard中可见的可重用自定义视图。
另一个优点是,您可以在CustomViewController类中实现逻辑并设置所有委托和视图准备工作,而无需创建单独的(在项目中难以找到的)控制器类,并且不需要将样板代码放置在主UIViewController中使用组件。我认为这对于可重用组件,例如嵌入在其他视图中的音乐播放器组件(小部件),非常有用。
目前最好的解决方案是使用自定义视图控制器,并在xib中定义其视图,然后在将视图控制器添加到storyboard中时删除Xcode创建的“view”属性(但不要忘记设置自定义类的名称)。
这将使运行时自动查找xib并加载它。您可以将此技巧用于任何类型的容器视图或内容视图。
根据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.h
、MyView.m
和MyView.xib
。
首先按照Ben Patch的回答所说,准备好您的MyView.xib
,将类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,由于Xcode >= 9.2中的这个错误,此视图的预览将无法在Storyboard中显示:https://forums.developer.apple.com/thread/95616
CustomView
类,并在xib中拥有它的主实例以及所有子视图和输出口。然后,您可以将该类应用于故事板或其他xib中的任何实例。UIView
更改为NibView
(或从UITableViewCell
更改为NibTableViewCell
)您可以在这里获取开源的BFWControls框架:https://github.com/BareFeetWare/BFWControls
如果您感兴趣,这里是驱动它的NibReplaceable
代码的简单提取:https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4
Tom
即使您的类名与XIB不同,也可以使用此解决方案。
例如,如果您有一个基础视图控制器类controllerA,它具有XIB名称controllerA.xib,并且您使用controllerB对其进行了子类化,并希望在storyboard中创建controllerB的实例,则可以执行以下操作:
*
- (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];
}
}