我想在故事板中的多个viewcontrollers中使用视图。因此,我考虑在外部xib中设计视图,以便在每个viewcontroller中反映更改。但是如何从故事板中的外部xib加载视图呢?它甚至可能吗?如果不是这样的话,还有哪些其他选择可以适应这种情况呢?
答案 0 :(得分:99)
我的完整示例是here,但我将在下面提供摘要。
<强>布局强>
为项目添加一个名称相同的.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
并将类名设置为自定义类名。
答案 1 :(得分:51)
有一段时间Christopher Swasey's approach是我找到的最佳方法。我问了我的团队中的几位资深开发员,其中一位有完美的解决方案!它满足了Christopher Swasey如此雄辩地解决的每一个问题,并且它不需要样板子类代码(我主要关注他的方法)。有一个问题,但除此之外,它非常直观且易于实施。
MyCustomClass.swift
MyCustomClass.xib
File's Owner
设置为自定义类(MyCustomClass
)class
值(在identity Inspector
下)留空。 因此,您的自定义视图将没有指定的类,但它将具有指定的文件所有者。 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并将.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 Segue
并StoryboardReference
到此自定义视图控制器,该视图应放在其他视图中在主要故事板中查看。
然后我们可以通过准备segue 设置委托以及此嵌入式ViewController和主视图控制器之间的通信。这种方法不同然后显示UIView,但更简单和更有效(从编程角度来看)可以用来实现相同的目标,即具有在主故事板中可见的可重用自定义视图
另外一个优点是你可以在CustomViewController类中实现你的逻辑,并且可以设置所有委托和视图准备,而无需创建单独的(在项目中更难找到)控制器类,也无需使用Component在主UIViewController中放置样板代码。我认为这对可重复使用的组件很有用。可嵌入其他视图的音乐播放器组件(小部件)。
答案 6 :(得分:1)
尽管最流行的答案很有效,但它们是错误的。他们都使用File's owner
作为类的出口和UI组件之间的连接,这是非常错误的。 File's owner
应该仅用于顶级对象,而不能用于UIView
。签出Apple developer document。
将UIView设置为File's owner
会导致这些不良后果。
contentView
的情况下,您被迫使用self
。这不仅很丑陋,而且在结构上也是错误的,因为数据结构可以更好地传达其UI结构。有一种不用使用File's owner
的优雅方法。请检查此blog post。它说明了如何正确执行操作。
答案 7 :(得分:0)
即使您的类与XIB的名称不同,也可以使用此解决方案。 例如,如果您有一个基本视图控制器类controllerA,它具有XIB名称controllerA.xib,并且您使用controllerB将其子类化,并且想要在故事板中创建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];
}
}
答案 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.h
,MyView.m
和MyView.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,或将自定义视图的实例添加为其自身的子视图。
只需执行以下操作:
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
汤姆