我想在情节提要中的多个视图控制器中使用一个视图。因此,我考虑过在外部Xib中设计视图,以使更改反映在每个ViewController中。但是如何从情节提要中的外部xib加载视图,甚至有可能?如果不是这种情况,还有哪些其他替代方法可以适应上述情况?
我想在情节提要中的多个视图控制器中使用一个视图。因此,我考虑过在外部Xib中设计视图,以使更改反映在每个ViewController中。但是如何从情节提要中的外部xib加载视图,甚至有可能?如果不是这种情况,还有哪些其他替代方法可以适应上述情况?
Answers:
我的完整示例在这里,但是我将在下面提供摘要。
布局
将.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
.xib文件的设置为您的自定义类(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并将class
.xib文件中的值设置为您的自定义类,则它将不会在情节提要中绘制,并且EXC_BAD_ACCESS
在运行应用程序时会出现错误,因为它陷入了无限循环中尝试使用该init?(coder aDecoder: NSCoder)
方法从笔尖初始化该类,该方法然后调用Self.nib.instantiate
并init
再次调用。
setupFromNib()
,似乎可以通过自动调整包含XIB创建的视图的表视图单元的大小来解决某些奇怪的自动布局问题。
@IBDesignable
兼容性。无法相信为什么在添加UIView文件时Xcode或UIKit没有提供像这样的默认值。
假设您已创建要使用的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(右上角的第三个图标,看起来像是带有线条的矩形),然后在“自定义类别”下的“类别”中输入子类别的名称。
xibView.frame = self.frame;
应该为xibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
,否则在将视图添加到情节提要中时,xibView将具有偏移量。
我一直发现“添加为子视图”解决方案并不令人满意,因为它带有(1)自动布局,(2)@IBInspectable
和(3)插座。相反,让我介绍你的魔法awakeAfter:
,一个NSObject
方法。
awakeAfter
可让您将从NIB / Storyboard唤醒的实际对象完全替换为其他对象。该对象,然后通过水合过程的说,已awakeFromNib
调用它,加入作为视图等
我们可以在视图的“纸板剪切”子类中使用它,其唯一目的是从NIB加载视图并将其返回以供在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
}
}
here️这里的一个重大缺点是,如果在情节提要中定义与另一个视图不相关的宽度,高度或纵横比约束,则必须手动复制它们。关联两个视图的约束安装在最近的公共祖先上,并且视图从内而外从情节提要中唤醒,因此,当这些约束在超级视图上合并时,交换已经发生。仅牵涉到该视图的约束直接安装在该视图上,因此,除非复制它们,否则在发生交换时会被扔掉。
请注意,这里发生的是将安装在情节提要中的视图上安装的约束复制到新实例化的视图,该视图可能已经具有其自身的约束(在其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
}
}
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
}
}
instantiateViewFromNib
不会返回任何内容。无论哪种方式,对于IMO来说都没什么大不了的,子类只是连接到情节提要上的一项便利,所有代码都应位于原始类上。
MyCustomView
。在我的xib中,默认情况下缺少左侧的内侧边栏;要打开它,底部/左侧附近的“查看方式:iPhone 7”特征控件旁边有一个按钮。
我想alternative
利用XIB views
被使用View Controller
在不同的故事板。
然后在主脚本中代替自定义视图container view
,Embed Segue
并且必须StoryboardReference
将此定制视图控制器与哪个视图放置在主脚本中的其他视图内。
然后,我们可以通过准备segue在此嵌入式ViewController和主视图控制器之间建立委派和通信。这种方法与显示UIView 有所不同,但是(从编程的角度来看)可以更简单,更有效地实现相同的目标,即具有可重复使用的自定义视图,该视图在主故事板上可见
另一个好处是,您可以在CustomViewController类中实现逻辑,并在那里设置所有委派和视图准备,而无需创建单独的(在项目中很难找到)控制器类,也无需使用Component将样板代码放置在主UIViewController中。我认为这对于可重用的组件很有用。可嵌入到其他视图中的Music Player组件(类似Widget)。
尽管最流行的答案很好用,但从概念上讲它们是错误的。它们都File's owner
用作类出口和UI组件之间的连接。File's owner
应该只用于UIView
s的顶级对象。查看Apple开发人员文档。使用UIView File's owner
会导致这些不良后果。
contentView
在应使用的地方使用self
。这不仅丑陋,而且在结构上也是错误的,因为中间视图使数据结构无法传达其UI结构。就像声明性UI的相反。有一种无需使用即可完成的优雅方法File's owner
。请检查此博客文章。它说明了如何正确执行操作。
这就是您一直想要的答案。您可以只创建您的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
汤姆👣
即使您的类与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];
}
}
根据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
。
首先MyView.xib
按照Ben Patch的回答说,准备您的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,此视图的预览将不会在Storyboard中显示,因为Xcode> = 9.2中存在以下错误:https ://forums.developer.apple.com/thread/95616