ストーリーボードの複数のViewControllerでビューを使用したい。したがって、変更がすべてのViewControllerに反映されるように、外部xibでビューを設計することを考えました。しかし、ストーリーボードの外部xibからビューを読み込むにはどうすればよいですか?そうでない場合は、上記の状況に合わせて他にどのような選択肢がありますか?
私の完全な例は 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
を追加して、クラス名をカスタムクラス名に設定するだけです。
しばらくの間 Christopher Swaseyのアプローチ は私が見つけた最高のアプローチでした。私のチームの数人の上級開発者にそれについて尋ねたところ、そのうちの一人は完璧な解決策! Christopher Swaseyが雄弁に対処した懸念事項をすべて満たし、定型的なサブクラスコードを必要としません(彼のアプローチに関する私の主な関心事)。 gochaがありますが、それ以外はかなり直感的で実装が簡単です。
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)
メソッドを使用してnibからクラスを初期化しようとする無限ループに陥り、Self.nib.instantiate
を呼び出してinit
を再度呼び出すためです。
使用したいxibを作成したと仮定します:
1)UIViewのカスタムサブクラスを作成します([ファイル]-> [新規]-> [ファイル...]-> [Cocoa Touchクラス]に移動できます。[サブクラス:]が[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番目のアイコンに線が入った長方形のように見えます)に移動します。 「カスタムクラス」の下の「クラス」としてサブクラスの名前を入力します。
(1)autolayout、(2)@IBInspectable
、および(3)アウトレットをねじ込んでいるのを見ると、「サブビューとして追加」ソリューションが不十分であることが常にわかっています。代わりに、NSObject
メソッドであるawakeAfter:
の魔法を紹介します。
awakeAfter
を使用すると、NIB /ストーリーボードから実際に起動されたオブジェクトを別のオブジェクトと完全に交換できます。 Thatその後、オブジェクトはハイドレーションプロセスにかけられ、awakeFromNib
が呼び出され、ビューとして追加されます。
これをビューの「ボール紙カットアウト」サブクラスで使用できます。その唯一の目的は、NIBからビューをロードし、ストーリーボードで使用するために返すことです。埋め込み可能なサブクラスは、元のクラスではなく、ストーリーボードビューのIDインスペクターで指定されます。これが機能するために実際にサブクラスである必要はありませんが、サブクラスにすることで、IBがIBInspectable/IBOutletプロパティを参照できるようになります。
この余分なボイラープレートは次善のように思えるかもしれません。ある意味、理想的にはUIStoryboard
がこれをシームレスに処理するためです。しかし、元のNIBおよびUIView
サブクラスを完全に変更しないという利点があります。それが果たす役割は、基本的にアダプターまたはブリッジクラスの役割であり、後悔している場合でも、追加のクラスとして設計上完全に有効です。反対に、クラスを節約したい場合は、@ BenPatchのソリューションは、いくつかの小さな変更を加えたプロトコルを実装することで機能します。どのソリューションがより良いのかという問題は、プログラマーのスタイルの問題に要約されます。それは、オブジェクトの構成と多重継承のどちらを好むかです。
注:NIBファイルのビューに設定されたクラスは同じままです。埋め込み可能なサブクラスは、ストーリーボードで使用されるonlyです。サブクラスを使用してコードでビューをインスタンス化することはできません。したがって、サブクラス自体に追加のロジックはありません。 のみには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のオブジェクトをループするだけです。ジェネリックタイプはreturn値であるため、コールサイトでタイプを指定する必要があることに注意してください。
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
}
}
XIB views
を使用するためのalternative
がView Controller
を使用することを考えています個別のストーリーボードで。
次に、メインビューのカスタムビューの代わりにcontainer view
をEmbed Segue
とともに使用し、これにStoryboardReference
を指定しますカスタムビューコントローラーメインの他のビュー内に配置するビュー絵コンテ。
次に、委任を設定と、この埋め込みViewControllerとメインView Controller間の通信をセグエの準備を使用して行います。このアプローチはdifferentその後UIViewを表示しますが、はるかにシンプルで効率的に(プログラミングの観点から)同じ目標を達成するために利用できます。つまり、メインストーリーボードに表示される再利用可能なカスタムビューがあります。
追加の利点は、CustomViewControllerクラスにロジックを実装でき、個別の(プロジェクトで見つけるのが難しい)コントローラークラスを作成せずに、コンポーネントを使用してボイラープレートコードをメインUIViewControllerに配置することなく、すべての委任とビューの準備を設定できることです。再利用可能なコンポーネントに適していると思います。他のビューに埋め込み可能な音楽プレーヤーコンポーネント(ウィジェットのような)。
現在、最善の解決策は、ビューをxibで定義したカスタムビューコントローラーを使用し、View Controllerを追加するときにストーリーボード内でXcodeが作成する "view"プロパティを削除することです(名前の設定を忘れないでくださいただし、カスタムクラス)。
これにより、ランタイムは自動的にxibを探してロードします。このトリックは、あらゆる種類のコンテナビューまたはコンテンツビューに使用できます。
このソリューションは、クラスにXIBと同じ名前がない場合でも使用できます。たとえば、コントローラーA.xibという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];
}
}
ここにあなたがずっと望んでいた答えがあります。 CustomView
クラスを作成するだけで、すべてのサブビューとアウトレットを持つxibにそのマスターインスタンスを保持できます。その後、そのクラスをストーリーボードまたは他のxibのインスタンスに適用できます。
File's Ownerをいじったり、アウトレットをプロキシに接続したり、特有の方法で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
トム????
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の応答 として準備します。この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];
警告!このビューのプレビューは、Xcode> = 9.2のこのバグのためにWatchKit拡張機能を使用する場合、ストーリーボードに表示されません: https://forums.developer.Apple .com/thread/95616
最も人気のある回答はうまくいきますが、間違っています。それらはすべて、クラスのアウトレットとUIコンポーネント間の接続としてFile's owner
を使用しますが、これは非常に間違っています。 File's owner
は、UIView
sではなく、トップレベルのオブジェクトにのみ使用されることになっています。 Apple開発者ドキュメント をご覧ください。 UIViewをFile's owner
として使用すると、これらの望ましくない結果につながります。
contentView
のみを使用することになっている場合、self
を使用する必要があります。いです。File's owner
を使用せずにそれを行うエレガントな方法があります。これを確認してください ブログ投稿 。正しい方法を説明しています。