ストーリーボードの複数のビューコントローラー全体でビューを使用したい。したがって、外部のxibでビューを設計して、すべてのビューコントローラーに変更が反映されるようにすることを考えました。しかし、どのようにしてストーリーボードの外部xibからビューをロードできますか?そうでない場合、上記の状況に合わせて他にどのような選択肢がありますか?
ストーリーボードの複数のビューコントローラー全体でビューを使用したい。したがって、外部のxibでビューを設計して、すべてのビューコントローラーに変更が反映されるようにすることを考えました。しかし、どのようにしてストーリーボードの外部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
、クラス名をカスタムクラス名に設定します。
init(frame:)
ます。詳細については、このチュートリアルを参照してください。
しばらくの間、クリストファー・スウェイジーのアプローチが私が見つけた最良のアプローチでした。私は私のチームの上級開発者の何人かにそれについて尋ねました、そして、彼らのうちの一人は完璧な解決策を持っていました!Christopher Swaseyが雄弁に取り組んだすべての懸念を満たし、ボイラープレートサブクラスコードを必要としません(彼のアプローチに関する私の主な懸念)。1つの落とし穴がありますが、それ以外はかなり直感的で実装が簡単です。
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
呼び出して呼び出すメソッドを使用してnibからクラスを初期化しようとしていますinit
。
setupFromNib()
、XIBで作成されたビューを含むテーブルビューのセルの自動サイズ設定に関する特定の奇妙な自動レイアウトの問題を修正しているようです。
@IBDesignable
互換性が大好きです。XViewまたはUIKitがUIViewファイルを追加するときにデフォルトでこのようなものを提供しない理由を信じられません。
使用したいxibを作成したと仮定します。
1)UIViewのカスタムサブクラスを作成します([ファイル]-> [新規]-> [ファイル...]-> [Cocoa Touch Class]に移動できます。「のサブクラス:」が「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を追加し、新しく追加したビューを選択して、IDインスペクターに移動します(右上の3番目のアイコンで、線が入った長方形のように見えます)。そして、「カスタムクラス」の下の「クラス」としてサブクラスの名前を入力します。
xibView.frame = self.frame;
である必要がありxibView.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
、そうでない場合xibViewは、ビューがストーリーボードに追加されたときにオフセットしています。
(サブビューとして追加する)ソリューションは、(1)オートレイアウト、(2)@IBInspectable
、(3)コンセントでねじ込まれるので、満足のいくものではありませんでした。代わりに、私はの魔法をご紹介しましょうawakeAfter:
AN、NSObject
方法。
awakeAfter
NIB /ストーリーボードから実際に起こされたオブジェクトを、別のオブジェクトと完全に入れ替えることができます。その後、そのオブジェクトはハイドレーションプロセスを通過し、awakeFromNib
それを呼び出し、ビューとして追加されます。
これをビューの「厚紙カットアウト」サブクラスで使用できます。その唯一の目的は、NIBからビューをロードして、ストーリーボードで使用するために返すことです。埋め込み可能なサブクラスは、元のクラスではなく、ストーリーボードビューのIDインスペクターで指定されます。これが機能するために実際にサブクラスである必要はありませんが、サブクラスにすることで、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️ここでの1つの重要な欠点は、ストーリーボードで幅、高さ、またはアスペクト比の制約を定義し、別のビューに関連しない場合、それらを手動でコピーする必要があることです。2つのビューに関連する制約は最も近い共通の祖先にインストールされ、ビューはストーリーボードから裏返しに起こされるため、これらの制約がスーパービューでハイドレートされるときまでに、スワップはすでに発生しています。問題のビューのみを含む制約は、そのビューに直接インストールされるため、コピーが作成されない限り、スワップが発生すると破棄されます。
ここで起こっているのは、ストーリーボードのビューにインストールされている制約が、新しくインスタンス化されたビューにコピーされていることに注意してください。それらは影響を受けません。
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とメインView Controller間の委任と通信を設定できます。このアプローチはUIViewの表示とは異なりますが、(プログラミングの観点から)はるかにシンプルで効率的に同じ目的を達成できます。つまり、メインストーリーボードに表示される再利用可能なカスタムビューがあります。
追加の利点は、CustomViewControllerクラスにロジックを実装でき、個別の(プロジェクトでは見つけにくい)コントローラークラスを作成することなく、コンポーネントを使用してメインのUIViewControllerにボイラープレートコードを配置することなく、すべての委任とビューの準備を設定できることです。これは、再利用可能なコンポーネントの元に適していると思います。他のビューに埋め込むことができる音楽プレーヤーコンポーネント(ウィジェットのような)。
最も人気のある回答は問題なく機能しますが、概念的には間違っています。これらはすべてFile's owner
、クラスのアウトレットとUIコンポーネント間の接続として使用されます。s File's owner
ではなくトップレベルのオブジェクトにのみ使用されることになっていますUIView
。Apple開発者向けドキュメントを確認してください。UIViewを使用File's owner
すると、これらの望ましくない結果につながります。
contentView
はずの場所での使用を余儀なくされていますself
。中間ビューではデータ構造がUI構造を伝達できないため、醜いだけでなく構造的にも誤りです。宣言型UIの逆のようなものです。を使用せずにそれを行うためのエレガントな方法がありFile's owner
ます。こちらのブログ投稿をご確認ください。それを正しく行う方法を説明します。
ここにあなたがずっと望んでいた答えがあります。CustomView
クラスを作成し、すべてのサブビューとアウトレットを備えたxibにそのクラスのマスターインスタンスを置くことができます。次に、そのクラスをストーリーボードまたは他のxibのインスタンスに適用できます。
ファイルのオーナーをいじったり、アウトレットをプロキシに接続したり、xibを特別な方法で変更したり、カスタムビューのインスタンスをそれ自体のサブビューとして追加したりする必要はありません。
これを行うだけです:
UIView
するNibView
(またはからUITableViewCell
のNibTableViewCell
)それでおしまい!
ストーリーボードでの設計時にカスタムビュー(xibからのサブビューを含む)を参照するためにIBDesignableとも連携します。
詳しくは、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と同じ名前がない場合でも使用できます。たとえば、XIB名がcontrollerA.xibであるベースビューコントローラークラスcontrollerAがあり、これを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内のメインビューではなく、Fileの所有者のクラスを設定してください。
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];
警告!XcodeのこのバグのためにWatchKit拡張機能を使用している場合、このビューのプレビューはストーリーボードに表示されません> = 9.2:https ://forums.developer.apple.com/thread/95616