如何在情节提要场景中嵌入自定义视图 xib?
Posted
技术标签:
【中文标题】如何在情节提要场景中嵌入自定义视图 xib?【英文标题】:How to embed a custom view xib in a storyboard scene? 【发布时间】:2015-03-04 16:02:38 【问题描述】:我是 XCode/ios 领域的新手;我已经完成了一些相当大的基于故事板的应用程序,但我从来没有在整个 nib/xib 事情上咬牙切齿。我想对场景使用相同的工具来设计/布局可重用的视图/控件。所以我为我的视图子类创建了我的第一个 xib 并将其绘制出来:
我已经连接了插座并设置了约束,就像我在故事板中习惯的那样。我将File Owner
的类设置为我的自定义UIView
子类。所以我假设我可以使用一些 API 来实例化这个视图子类,并且它会如图所示进行配置/连接。
现在回到我的故事板,我想嵌入/重用它。我在表格视图原型单元格中这样做:
我有一个看法。我已经将它的类设置为我的子类。我已经为它创建了一个出口,所以我可以操纵它。
64 美元的问题是我在哪里/如何表明仅将视图子类的空/未配置实例放在那里是不够的,而是使用我创建的 .xib 来配置/实例化它?如果在 XCode6 中,我可以只输入 XIB 文件以用于给定的 UIView,那就太酷了,但我没有看到这样做的字段,所以我假设我必须在某处的代码中做一些事情。
(我确实在 SO 上看到过类似的其他问题,但没有发现任何关于这部分难题的问题,或者是最新的 XCode6/2015)
更新
我可以通过如下实现表格单元格的awakeFromNib
来完成这项工作:
- (void)awakeFromNib
// gather all of the constraints pointing to the uncofigured instance
NSArray* progressConstraints = [self.contentView.constraints filteredArrayUsingPredicate: [NSPredicate predicateWithBlock:^BOOL(id each, NSDictionary *_)
return (((NSLayoutConstraint*)each).firstItem == self.progressControl) || (((NSLayoutConstraint*)each).secondItem == self.progressControl);
]];
// fetch the fleshed out variant
ProgramProgressControl *fromXIB = [[[NSBundle mainBundle] loadNibNamed:@"ProgramProgressControl" owner:self options:nil] objectAtIndex:0];
// ape the current placeholder's frame
fromXIB.frame = self.progressControl.frame;
// now swap them
[UIView transitionFromView: self.progressControl toView: fromXIB duration: 0 options: 0 completion: nil];
// recreate all of the constraints, but for the new guy
for (NSLayoutConstraint *each in progressConstraints)
id firstItem = each.firstItem == self.progressControl ? fromXIB : each.firstItem;
id secondItem = each.secondItem == self.progressControl ? fromXIB : each.secondItem;
NSLayoutConstraint *constraint = [NSLayoutConstraint constraintWithItem: firstItem attribute: each.firstAttribute relatedBy: each.relation toItem: secondItem attribute: each.secondAttribute multiplier: each.multiplier constant: each.constant];
[self.contentView addConstraint: constraint];
// update our outlet
self.progressControl = fromXIB;
那么这很容易吗?还是我为此付出了太多努力?
【问题讨论】:
1) 你不应该指定文件的所有者,因为如果 xib 只是自定义视图,你就没有文件的所有者。当你加载 nib owner 参数时应该是 nil。 2)您应该为您的视图指定自定义类。并将插座连接到此视图 【参考方案1】:你快到了。您需要在将视图分配给的自定义类中覆盖 initWithCoder。
- (id)initWithCoder:(NSCoder *)aDecoder
if ((self = [super initWithCoder:aDecoder]))
[self addSubview:[[[NSBundle mainBundle] loadNibNamed:@"ViewYouCreated" owner:self options:nil] objectAtIndex:0]];
return self;
一旦完成,StoryBoard 就会知道在 UIView 中加载 xib。
这里有更详细的解释:
这就是你的UIViewController
在你的故事板上的样子:
蓝色空间基本上是一个 UIView,它将“容纳”您的 xib。
这是你的 xib:
有一个动作连接到它上面的一个按钮,它会打印一些文本。
这是最终的结果:
第一个clickMe和第二个的区别是第一个是用StoryBoard
添加到UIViewController
的。第二个是使用代码添加的。
【讨论】:
我不知道它是否会起作用,但你不能在layoutSubviews
而不是initWithCoder:
中添加视图并创建类IB_DESIGNABLE
吗?我想这取决于IB_DESIGNABLE
类是否能够从包中加载
我不知道我是否误用了您的答案,但它没有按预期工作。在一种情况下,我将它放在 .xib 已注册为文件所有者的 ProgramProgressControl
类中。在这种情况下,它在super
发送时引发了异常。另一方面,我将它放置在要放置的表格单元子类中。视图有点出现,但甚至不在外壳内部,而且我似乎没有被连接到插座。
我不确定您的代码是如何工作的,但我为您找到了一个完整的示例,它演示了 initWithCoder and
和 xib
文件的工作原理。 ***.com/questions/9251202/…
@TravisGriggs 我已经用更多示例和可以玩的示例项目编辑了我的答案。
或者,您可以覆盖 awakeAfterUsingCoder: 并进行实际的视图交换,而不是覆盖 initWithCoder。相同的工作量,更好的结果。【参考方案2】:
您需要在您的自定义UIView
子类中实现awakeAfterUsingCoder:
。此方法允许您将解码的对象(来自情节提要)与不同的对象(来自可重用的 xib)交换,如下所示:
- (id) awakeAfterUsingCoder: (NSCoder *) aDecoder
// without this check you'll end up with a recursive loop - we need to know that we were loaded from our view xib vs the storyboard.
// set the view tag in the MyView xib to be -999 and anything else in the storyboard.
if ( self.tag == -999 )
return self;
// make sure your custom view is the first object in the nib
MyView* v = [[[UINib nibWithNibName: @"MyView" bundle: nil] instantiateWithOwner: nil options: nil] firstObject];
// copy properties forward from the storyboard-decoded object (self)
v.frame = self.frame;
v.autoresizingMask = self.autoresizingMask;
v.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
v.tag = self.tag;
// copy any other attribtues you want to set in the storyboard
// possibly copy any child constraints for width/height
return v;
有一篇很好的文章 here 讨论了这种技术和一些替代方法。
此外,如果您将IB_DESIGNABLE
添加到您的@interface 声明中,并提供initWithFrame:
方法,您可以获得设计时预览以在IB 中工作(需要Xcode 6!):
IB_DESIGNABLE @interface MyView : UIView
@end
@implementation MyView
- (id) initWithFrame: (CGRect) frame
self = [[[UINib nibWithNibName: @"MyView"
bundle: [NSBundle bundleForClass: [MyView class]]]
instantiateWithOwner: nil
options: nil] firstObject];
self.frame = frame;
return self;
【讨论】:
这绝对比“使用第二个包装视图并将您的真实视图添加为 initWithCoder 中的子视图” slapdashery 更可取。任何繁重的上下文移植都可以在类别/扩展方法中完成。删除一些配置的一个建议:大概您在 xib 中有子视图,否则将毫无意义,因此可能会依赖子视图为空或不为空,而不是 tag 属性。 不幸的是,这不可能。【参考方案3】:界面生成器和 Swift 4 的一种非常酷且可重用的方式:
像这样创建一个新类:
import Foundation
import UIKit
@IBDesignable class XibView: UIView
@IBInspectable var xibName: String?
override func awakeFromNib()
guard let name = self.xibName,
let xib = Bundle.main.loadNibNamed(name, owner: self),
let view = xib.first as? UIView else return
self.addSubview(view)
在您的故事板中,添加一个 UIView 作为 Xib 的容器。给它一个类名XibView
:
在这个新的XibView
的属性检查器中,在 IBInspectable 字段中设置 .xib 的名称(不带文件扩展名):
为您的项目添加一个新的 Xib 视图,并在属性检查器中,将 Xib 的“文件所有者”设置为 XibView
(确保您只将“文件所有者”设置为您的自定义类,不要子类化内容视图,否则会崩溃),然后再次设置 IBInspectable 字段:
需要注意的一点:这假设您将 .xib 框架与其容器相匹配。如果您不这样做,或者需要调整大小,则需要添加一些编程约束或修改子视图的框架以适应。我使用snapkit 让事情变得简单:
xibView.snp_makeConstraints(closure: (make) -> Void in
make.edges.equalTo(self)
)
奖励积分
据称您可以使用prepareForInterfaceBuilder()
使这些可重用视图在Interface Builder 中可见,但我运气不佳。 This blog 建议添加 contentView
属性,并调用以下内容:
override func prepareForInterfaceBuilder()
super.prepareForInterfaceBuilder()
xibSetup()
contentView?.prepareForInterfaceBuilder()
【讨论】:
非常有趣的解决方案,虽然我收到警告:“IB Designables: Ignoring user defined runtime attribute for key path "xibName" on the instance of "UIView"。尝试设置其时遇到异常value: [if let views = xib as? [UIView] where views.count > 0
在 swift 语法中更改为 if let views = xib as? [UIView], views.count > 0
【参考方案4】:
您只需将 UIView
拖放到您的 IB 中,然后将其导出并设置
yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject];
[self.view addSubview:yourView]
步骤
-
添加新文件 => 用户界面 => UIView
设置自定义类 - yourUIViewClass
设置恢复 ID - yourUIViewClass
yourUIViewClass *yourView = [[[NSBundle mainBundle] loadNibNamed:@"yourUIViewClass" owner:self options:nil] firstObject];
[self.view addSubview:yourView]
现在您可以根据需要自定义视图。
【讨论】:
我必须在viewDidLoad
之前设置插座的值吗?还是之后?还是没关系?
在viewDidLoad
之后:因为如果视图还没有加载,你不能在上面添加另一个子视图。
您在这里展示的是如何添加从 XIB 获取的视图作为 子视图 到我在情节提要中创建的视图和有一个outlet
到。这很酷。但我想要的是用 XIB 视图替换情节提要中的视图。 IOW,我想像占位符一样使用情节提要中的视图。也就是说,我希望应用到情节提要中的视图的属性(例如背景颜色、布局等)仍然适用。【参考方案5】:
多年来,我一直在使用此代码 sn-p。如果您打算在 XIB 中使用自定义类视图,只需将其放在自定义类的 .m 文件中即可。
作为副作用,它会导致调用 awakeFromNib,因此您可以将所有初始化/设置代码留在其中。
- (id)awakeAfterUsingCoder:(NSCoder*)aDecoder
if ([[self subviews] count] == 0)
UIView *view = [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:nil options:nil][0];
view.frame = self.frame;
view.autoresizingMask = self.autoresizingMask;
view.alpha = self.alpha;
view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;
return view;
return self;
【讨论】:
【参考方案6】: 创建 xib 文件File > New > New File > iOS > User Interface > View
创建自定义 UIView 类
File > New > New File > iOS > Source > CocoaTouch
将 xib 文件的标识分配给自定义视图类
在视图控制器的 viewDidLoad 中,使用 loadNibNamed:
on NSBundle.mainBundle
初始化 xib 及其关联文件,返回的第一个视图可以添加为 self.view 的子视图。
从 nib 加载的自定义视图可以保存到 viewDidLayoutSubviews
中设置框架的属性。只需将框架设置为self.view
的框架,除非您需要使其小于self.view
。
class ViewController: UIViewController
weak var customView: MyView!
override func viewDidLoad()
super.viewDidLoad()
self.customView = NSBundle.mainBundle().loadNibNamed("MyView", owner: self, options: nil)[0] as! MyView
self.view.addSubview(customView)
addButtonHandlerForCustomView()
private func addButtonHandlerForCustomView()
customView.buttonHandler =
[weak self] (sender:UIButton) in
guard let welf = self else
return
welf.buttonTapped(sender)
override func viewDidLayoutSubviews()
self.customView.frame = self.view.frame
private func buttonTapped(button:UIButton)
此外,如果您想从 xib 与您的 UIViewController
实例对话,请在自定义视图的类上创建一个弱属性。
class MyView: UIView
var buttonHandler:((sender:UIButton)->())!
@IBAction func buttonTapped(sender: UIButton)
buttonHandler(sender:sender)
这是GitHub上的项目
【讨论】:
【参考方案7】:@brandonscript 的想法更快速的版本,提前返回:
override func awakeFromNib()
guard let xibName = xibName,
let xib = Bundle.main.loadNibNamed(xibName, owner: self, options: nil),
let views = xib as? [UIView] else
return
if views.count > 0
self.addSubview(views[0])
【讨论】:
【参考方案8】:虽然我不推荐你要走的路径,但可以通过在你希望视图出现的位置放置一个“嵌入式视图控制器视图”来完成。
嵌入一个包含单个视图的视图控制器——您想要重用的视图。
【讨论】:
此方法有效,直到您想将它们放入表格单元格之类的东西中:)【参考方案9】:这个问题已经有一段时间了,我已经看到了很多答案。我最近重新访问了它,因为我刚刚使用 UIViewController 嵌入。哪个有效,直到您想将某些内容放入“在运行时重复的元素”中(例如 UICollectionViewCell 或 UITableViewCell)。 @TomSwift 提供的链接让我遵循了
的模式A) 与其将父视图类设为自定义类类型,不如将 FileOwner 设为目标类(在我的示例中为 CycleControlsBar)
B) 嵌套小部件的任何出口/动作链接都指向那个
C) 在 CycleControlsBar 上实现这个简单的方法:
required init?(coder aDecoder: NSCoder)
super.init(coder: aDecoder)
if let container = (Bundle.main.loadNibNamed("CycleControlsBar", owner: self, options: nil) as? [UIView])?.first
self.addSubview(container)
container.constrainToSuperview() // left as an excise for the student
像魅力一样工作,比其他方法简单得多。
【讨论】:
【参考方案10】:这是您一直想要的答案。您可以只创建您的CustomView
类,将它的主实例放在带有所有子视图和插座的xib 中。然后,您可以将该类应用于情节提要或其他 xib 中的任何实例。
无需摆弄 File's Owner,或将 outlet 连接到代理或以特殊方式修改 xib,或添加自定义视图的实例作为其自身的子视图。
这样做:
-
导入 BFWControls 框架
将您的超类从
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
下面是驱动它的代码的简单摘录,以防您好奇: https://gist.github.com/barefeettom/f48f6569100415e0ef1fd530ca39f5b4
汤姆?
【讨论】:
【参考方案11】:“正确”的答案是您不打算使用相应的 nib 制作可重复使用的视图。如果视图子类作为可重用对象很有价值,那么它很少需要一个 nib 来配合它。以 UIKit 提供的每个视图子类为例。这种想法的一部分是实际上有价值的视图子类不会使用 nib 来实现,这是 Apple 的一般视图。
通常,当您在 nib 或情节提要中使用视图时,无论如何您都希望针对给定的用例以图形方式对其进行调整。
您可以考虑使用“复制粘贴”来重新创建相同或相似的视图,而不是制作单独的 nib。我相信这可以满足相同的要求,它会让你或多或少地与 Apple 的做法保持一致。
【讨论】:
我没有听从你的论点。您似乎在说“如果它是可重用的,只需将其全部编码为一个类(布局和所有)”,如果是这样的话,为什么不是所有视图控制器都以这种方式完成,以及任何有用的视图层次结构? 这是一个有趣的观点——可重用视图应该始终在代码中实现,并且 xib 和故事板仅用于从可重用视图组装不可重用视图。这与工具所启用的功能非常一致。但是你还有什么理由说这是“苹果的普遍看法”? 我认为关键是要找到一种方法来做到这一点,尽管苹果公司的普遍看法。这种方法似乎适得其反;重用具有模块化配置的 UIView xib 非常普遍,并且有很多应用程序。 “你为什么要这样做?”对于任何构建一个严肃的应用程序的人来说,这种方法在某些情况下必然不得不违背“苹果正在做的事情”,这种方法是无益的。 (除非您正在为 Apple 自己开发代码) Travis:我的意思是,如果您在 Interface Builder 中设计的视图有用,那么有一种更简单的方法可以复制它。您选择它,按 command-C,然后按 command-V。与将其放置在笔尖中相比,步骤很少。如果您有一个完全在所有地方都相同的视图,并且在很多地方都使用过,并且您希望能够轻松调整而不是有一种新的方法可以做到这一点。但在我开发过的许多应用程序中,并没有出现这种情况。 DivideByZer0:这很有帮助,因为作者试图将他们的工作流程强加到工具上,而不是接受工具。以上是关于如何在情节提要场景中嵌入自定义视图 xib?的主要内容,如果未能解决你的问题,请参考以下文章
从 Table View Cell xib 与披露指示符转至情节提要