xibを使用して再利用可能なUIViewを作成する(およびストーリーボードからロードする)


81

OK、これについてStackOverflowに数十の投稿がありますが、ソリューションについて特に明確なものはありません。UIView付随するxibファイルを使用してカスタムを作成したいと思います。要件は次のとおりです。

  • 個別ではありませんUIViewController–完全に自己完結型のクラス
  • ビューのプロパティを設定/取得できるようにするクラスのアウトレット

これを行うための私の現在のアプローチは次のとおりです。

  1. オーバーライド -(id)initWithFrame:

    -(id)initWithFrame:(CGRect)frame {
        self = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:self
                                            options:nil] objectAtIndex:0];
        self.frame = frame;
        return self;
    }
    
  2. -(id)initWithFrame:ビューコントローラで使用してプログラムでインスタンス化する

    MyCustomView *myCustomView = [[MyCustomView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
    [self.view insertSubview:myCustomView atIndex:0];
    

これは正常に機能します(ただし[super init]、ロードされたペン先のコンテンツを使用してオブジェクトを呼び出したり設定したりすることは少し疑わしいようですが、この場合はサブビュー追加することもできます)。ただし、ストーリーボードからのビューもインスタンス化できるようにしたいと思います。だから私はできる:

  1. UIViewストーリーボードの親ビューにを配置します
  2. カスタムクラスをに設定します MyCustomView
  3. オーバーライド-(id)initWithCoder:–私が最もよく見たコードは、次のようなパターンに適合します。

    -(id)initWithCoder:(NSCoder *)aDecoder {
        self = [super initWithCoder:aDecoder];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(id)initWithFrame:(CGRect)frame {
        self = [super initWithFrame:frame];
        if (self) {
            [self initializeSubviews];
        }
        return self;
    }
    
    -(void)initializeSubviews {
        typeof(view) view = [[[NSBundle mainBundle]
                             loadNibNamed:NSStringFromClass([self class])
                                    owner:self
                                  options:nil] objectAtIndex:0];
        [self addSubview:view];
    }
    

もちろん、これは機能しません。上記のアプローチを使用するか、プログラムでインスタンス化するかにかかわらず、どちらもファイルにペン先-(id)initWithCoder:を入力-(void)initializeSubviewsしてロードするときに再帰的に呼び出すことになります。

他のいくつかのSOの質問は、次のようなこれに対処ここではここではここここ。しかし、与えられた答えのどれも問題を十分に解決しません:

  • 一般的な提案は、クラス全体をUIViewControllerに埋め込み、そこでnibの読み込みを行うことですが、ラッパーとして別のファイルを追加する必要があるため、これは私には最適ではないようです。

誰かがこの問題を解決する方法についてアドバイスを与えることができますか、そしてUIView最小限の手間で/薄いコントローラーラッパーなしでカスタムで動作するアウトレットを手に入れることができますか?または、最小限の定型コードで物事を行うための代替のよりクリーンな方法はありますか?


1
これに対して満足のいく答えを得たことがありますか?私は今これに苦労しています。あなたが言うように、他のすべての答えは十分に良くないようです。過去数か月間に何かを見つけた場合は、いつでも自分で質問に答えることができます。
マイクマイヤーズ2014

13
iOSで再利用可能なビューを作成するのが難しいのはなぜですか?
時計職人2014


1
実際、リンク先の回答はまったく同じアプローチを使用しています(ただし、回答にはrect関数からの初期化は含まれていません。つまり、ストーリーボードからのみ初期化でき、プログラムでは初期化できません)
Ken Chatfield 2015

1
この非常に古いQAに関して、AppleはついにSTORYBOARDREFERENCESを導入しました... developer.apple.com/library/ios/recipes/… ...それで、おい!
Fattie 2016年

回答:


13

あなたの問題はloadNibNamed:(の子孫)からの呼び出しinitWithCoder:です。loadNibNamed:内部でinitWithCoder:。を呼び出します。ストーリーボードコーダーをオーバーライドし、常にxib実装をロードする場合は、次の手法をお勧めします。ビュークラスにプロパティを追加し、xibファイルで(ユーザー定義のランタイム属性で)事前に定義された値に設定します。ここで、呼び出した後[super initWithCoder:aDecoder];、プロパティの値を確認します。所定の値の場合は、を呼び出さないでください[self initializeSubviews];

だから、このようなもの:

-(instancetype)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];

    if (self && self._xibProperty != 666)
    {
        //We are in the storyboard code path. Initialize from the xib.
        self = [self initializeSubviews];

        //Here, you can load properties that you wish to expose to the user to set in a storyboard; e.g.:
        //self.backgroundColor = [aDecoder decodeObjectOfClass:[UIColor class] forKey:@"backgroundColor"];
    }

    return self;
}

-(instancetype)initializeSubviews {
    id view =   [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil] firstObject];

    return view;
}

ありがとう@LeoNatan!最初に述べた問題の最善の解決策であるため、私はこの回答を受け入れています。ただし、Swiftでは使用できなくなったことに注意してください。その場合の回避策について、いくつかの個別のメモを追加しました。
ケンチャットフィールド2014

@KenChatfield Swiftサブプロジェクトでそれに気づき、イライラしました。彼らが何を考えているのかわかりません。これがないと、SwiftではCocoa / CocoaTouchの内部実装の多くが不可能だからです。私の賭けは、バグではなく機能に焦点を当てる時間が実際にあるときに、いくつかの動的な機能があることです。Swiftはまったく準備ができておらず、最悪の犯罪者は開発ツールです。
レオナタン

はい、その通りです。Swiftには現在多くの荒削りな部分があります。そうは言っても、自分自身に直接割り当てるのはちょっとしたハックだったようですので、コンパイラがそのようなことに対して警告するようになったのは嬉しいことです(この厳密な型チェックは、言語)。ただし、カスタムビューとxibsのリンクが非常に面倒で、少し不十分になることは間違いありません。彼らがバグの整理を終えたら、このようなものでもう少し役立ついくつかのより動的な機能が表示されることを願っています!
ケンチャットフィールド2014

これはハックではなく、クラスクラスターの仕組みです。そして実際には、returnクラスのサブクラスであるオブジェクトのreturnを代わりに返すことを許可する技術的な問題はありません。現状では、CocoaとCocoa Touchの基礎の1つであるクラスクラスターを実装することは不可能です。ひどい。Core Dataなどの一部のフレームワークは、Swiftに実装できないため、ほとんどの用途で役に立たない言語になっています。
レオナタン2014

1
どういうわけかそれは私にとってはうまくいきませんでした(iOS8.1SDK)。ランタイム属性ではなく、XIBにrestorementIdentifierを設定しました。たとえば、initWithCoderではなくxibに「MyViewRestorationID」を設定しました。![[selfrestorementIdentifier] isEqualToString:@ "MyViewRestorationID"]
ingaham 2015年

26

このQA(多くの場合と同様)は、実際には歴史的な関心事であることに注意してください。

今日iOSでは何年も何年もの間、すべてが単なるコンテナビューです。ここに完全なチュートリアル

(確かに、AppleはついにStoryboard Referencesを追加しましたが、少し前になり、はるかに簡単になりました。)

これは、どこにでもコンテナビューがある典型的なストーリーボードです。すべてがコンテナビューです。それはあなたがアプリを作る方法です。

ここに画像の説明を入力してください

(好奇心として、KenCの回答は、実際には「自分に割り当てる」ことができないため、xibを一種のラッパービューにロードするために行われていた方法を正確に示しています。)


これに伴う問題は、コンテンツビューを埋め込んだすべてのViewControllerが大量に発生することです。
ボグダンオヌ2014年

こんにちは@BogdanOnu!多くの、多くの、多くのビューコントローラが必要です。「最小の」ものについては、ViewControllerが必要です。
Fattie 2014年

2
これは非常に便利です– @ JoeBlowに感謝します。コンテナビューを使用することは間違いなく代替アプローチのようであり、xibsを直接処理することのすべての複雑さを回避する簡単な方法です。ただし、すべてのUIデザインをストーリーボードに直接埋め込む必要があるため、プロジェクト間で配布/使用するための再利用可能なコンポーネントを作成するための100%満足のいく代替手段とは思えません。
ケンチャットフィールド2014

3
この場合、追加のViewControllerを使用しても問題は少なくなります。これは、xibの場合はカスタムViewクラスに属するプログラムロジックが含まれているだけですが、ストーリーボードとの緊密な結合は、私がコンテナビューがこの問題を完全に解決できるかどうかはわかりません。おそらくほとんどの実際的な状況では、ビューはプロジェクト固有であるため、これが最良かつ最も「標準的な」ソリューションですが、ストーリーボードで使用するためにカスタムビューをパッケージ化する簡単な方法がまだないことに驚いています。分割統治したいという私のプログラマーの衝動はかゆみです;)
Ken Chatfield 2014

1
また、Xcode 6にカスタムUIViewサブクラスのライブレンダリングが導入されたことで、この方法でxibsを使用してビューを作成することが非推奨になるという前提を購入するかどうかはわかりません
Ken Chatfield

24

Swiftのリリースで状況を更新するために、これを別の投稿として追加します。LeoNatanによって説明されたアプローチは、Objective-Cで完全に機能します。ただし、より厳密なコンパイル時間チェックにより、self、Swiftのxibファイルからロードするときに割り当てられなくなります。

その結果、自分自身を完全に置き換えるのではなく、xibファイルからロードされたビューをカスタムUIViewサブクラスのサブビューとして追加する以外に選択肢はありません。これは、元の質問で概説した2番目のアプローチに類似しています。このアプローチを使用したSwiftのクラスの大まかな概要は次のとおりです。

@IBDesignable // <- to optionally enable live rendering in IB
class ExampleView: UIView {

    required init(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        initializeSubviews()
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        initializeSubviews()
    }

    func initializeSubviews() {
        // below doesn't work as returned class name is normally in project module scope
        /*let viewName = NSStringFromClass(self.classForCoder)*/
        let viewName = "ExampleView"
        let view: UIView = NSBundle.mainBundle().loadNibNamed(viewName,
                               owner: self, options: nil)[0] as! UIView
        self.addSubview(view)
        view.frame = self.bounds
    }

}

このアプローチの欠点は、Objective-CでLeoNatanによって概説されたアプローチを使用する場合には存在しない、ビュー階層に追加の冗長レイヤーが導入されることです。ただし、これは必要な悪であり、Xcodeでの基本的な設計方法の産物と見なすことができます(一貫して機能する方法でカスタムUIViewクラスをUIレイアウトにリンクするのは非常に難しいので、私にはまだ気が狂っているようです。ストーリーボードとコードの両方で)–self以前は、初期化子でホールセールを置き換えることは、特に解釈可能な方法のようには見えませんでしたが、ビューごとに基本的に2つのビュークラスがあることもそれほど素晴らしいとは思えません。

それにもかかわらず、このアプローチの1つの幸せな結果は、に割り当てるときに正しい動作を保証するために、ビューのカスタムクラスをインターフェイスビルダーのクラスファイルに設定する必要がなくなったことです。そのselfため、init(coder aDecoder: NSCoder)発行時の再帰呼び出しloadNibNamed()が壊れます( xibファイルのカスタムクラス、init(coder aDecoder: NSCoder)。代わりに、カスタムバージョンではなくプレーンバニラUIViewが呼び出されます)。

xibに格納されているビューに対してクラスを直接カスタマイズすることはできませんが、ビューのファイル所有者をカスタムクラスに設定した後、アウトレット/アクションなどを使用してビューを「親」UIViewサブクラスにリンクすることはできます。

カスタムビューのファイル所有者プロパティを設定する

このアプローチを使用してこのようなビュークラスの実装を段階的に示すビデオは、次のビデオにあります。


こんにちはジョー–あなたのコメントに感謝します、それは大胆です(あなたの他の多くのコメントのように!)あなたの答えに答えて述べたように、私はほとんどの状況でコンテナビューがおそらく最良のアプローチであることに同意しますが、そのような場合ビューをプロジェクト間で使用する(または分散する)必要がある場合、少なくとも私にとっては意味があり、他の人にとっても代替手段があるように見えます。個人的には悪いスタイルだと思うかもしれませんが、参考のためにこれがどのように行われるかを示唆する他の多くの投稿をここに置いて、人々に自分で判断させることができます。どちらのアプローチも役立つようです。
ケンチャットフィールド2015年

これをありがとう。ペン先のクラスをとして残すことについてのアドバイスに耳を傾けるまで、Swiftでさまざまなアプローチを試しましたが成功しませんでしたUIView。Appleがこれを簡単にしたことがないのは非常識であり、今では事実上不可能であることに同意します。コンテナが常に答えであるとは限りません。
エシェロン

16

ステップ1。selfストーリーボードからの置き換え

メソッドの置換selfinitWithCoder:次のエラーで失敗します。

'NSGenericException', reason: 'This coder requires that replaced objects be returned from initWithCoder:'

代わりに、デコードされたオブジェクトをawakeAfterUsingCoder:(ではなくawakeFromNib)に置き換えることができます。お気に入り:

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

ステップ2。再帰呼び出しの防止

もちろん、これは再帰的な呼び出しの問題も引き起こします。(ストーリーボードデコード-> awakeAfterUsingCoder:-> loadNibNamed:-> awakeAfterUsingCoder:-> loadNibNamed:-> ...)
したがってawakeAfterUsingCoder:、ストーリーボードデコードプロセスまたはXIBデコードプロセスで現在の呼び出しが呼び出されていることを確認する必要があります。これを行うには、いくつかの方法があります。

a)@propertyNIBでのみ設定されているprivateを使用します。

@interface MyCustomView : UIView
@property (assign, nonatomic) BOOL xib
@end

「MyCustomView.xib」でのみ「ユーザー定義のランタイム属性」を設定します。

長所:

  • なし

短所:

  • 単に機能しません:AFTERsetXib:と呼ばれます awakeAfterUsingCoder:

b)selfサブビューがあるかどうかを確認します

通常、xibにはサブビューがありますが、ストーリーボードにはありません。

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(self.subviews.count > 0) {
        // loading xib
        return self;
    }
    else {
        // loading storyboard
        return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                              owner:nil
                                            options:nil] objectAtIndex:0];
    }
}

長所:

  • InterfaceBuilderにはトリックはありません。

短所:

  • ストーリーボードにサブビューを含めることはできません。

c)loadNibNamed:通話中に静的フラグを設定する

static BOOL _loadingXib = NO;

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    if(_loadingXib) {
        // xib
        return self;
    }
    else {
        // storyboard
        _loadingXib = YES;
        typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                           owner:nil
                                                         options:nil] objectAtIndex:0];
        _loadingXib = NO;
        return view;
    }
}

長所:

  • シンプル
  • InterfaceBuilderにはトリックはありません。

短所:

  • 安全ではありません:静的共有フラグは危険です

d)XIBでプライベートサブクラスを使用する

たとえば_NIB_MyCustomView、のサブクラスとして宣言しMyCustomViewます。また、XIBでのみ_NIB_MyCustomView代わりに使用MyCustomViewしてください。

MyCustomView.h:

@interface MyCustomView : UIView
@end

MyCustomView.m:

#import "MyCustomView.h"

@implementation MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In Storyboard decoding path.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

@interface _NIB_MyCustomView : MyCustomView
@end

@implementation _NIB_MyCustomView
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In XIB decoding path.
    // Block recursive call.
    return self;
}
@end

長所:

  • 明示的ifではありませんMyCustomView

短所:

  • 接頭辞_NIB_XIB Interface Builderでトリック
  • 比較的多くのコード

e)ストーリーボードのプレースホルダーとしてサブクラスを使用する

d)ストーリーボードのサブクラス、XIBの元のクラスと似ていますが使用します。

ここではMyCustomViewProto、のサブクラスとして宣言しMyCustomViewます。

@interface MyCustomViewProto : MyCustomView
@end
@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    // In storyboard decoding
    // Returns MyCustomView loaded from NIB.
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self superclass])
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}
@end

長所:

  • とても安全
  • 掃除; に余分なコードはありませんMyCustomView
  • ifと同じ明示的なチェックはありませんd)

短所:

  • ストーリーボードでサブクラスを使用する必要があります。

e)はそれが最も安全でクリーンな戦略だと思います。そこで、ここでそれを採用します。

ステップ3。プロパティをコピーする

loadNibNamed:'awakeAfterUsingCoder:'の後でself、ストーリーボードのインスタンスをデコードするいくつかのプロパティをコピーする必要があります。frame自動レイアウト/自動サイズ変更のプロパティは特に重要です。

- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    typeof(self) view = [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class])
                                                       owner:nil
                                                     options:nil] objectAtIndex:0];
    // copy layout properities.
    view.frame = self.frame;
    view.autoresizingMask = self.autoresizingMask;
    view.translatesAutoresizingMaskIntoConstraints = self.translatesAutoresizingMaskIntoConstraints;

    // copy autolayout constraints
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in self.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == self) firstItem = view;
        if(secondItem == self) secondItem = view;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }

    // move subviews
    for(UIView *subview in self.subviews) {
        [view addSubview:subview];
    }
    [view addConstraints:constraints];

    // Copy more properties you like to expose in Storyboard.

    return view;
}

最終的解決

ご覧のとおり、これはちょっとした定型コードです。それらを「カテゴリ」として実装できます。ここでは、一般的に使用されるUIView+loadFromNibコードを拡張します。

#import <UIKit/UIKit.h>

@interface UIView (loadFromNib)
@end

@implementation UIView (loadFromNib)

+ (id)loadFromNib {
    return [[[NSBundle mainBundle] loadNibNamed:NSStringFromClass(self)
                                          owner:nil
                                        options:nil] objectAtIndex:0];
}

- (void)copyPropertiesFromPrototype:(UIView *)proto {
    self.frame = proto.frame;
    self.autoresizingMask = proto.autoresizingMask;
    self.translatesAutoresizingMaskIntoConstraints = proto.translatesAutoresizingMaskIntoConstraints;
    NSMutableArray *constraints = [NSMutableArray array];
    for(NSLayoutConstraint *constraint in proto.constraints) {
        id firstItem = constraint.firstItem;
        id secondItem = constraint.secondItem;
        if(firstItem == proto) firstItem = self;
        if(secondItem == proto) secondItem = self;
        [constraints addObject:[NSLayoutConstraint constraintWithItem:firstItem
                                                            attribute:constraint.firstAttribute
                                                            relatedBy:constraint.relation
                                                               toItem:secondItem
                                                            attribute:constraint.secondAttribute
                                                           multiplier:constraint.multiplier
                                                             constant:constraint.constant]];
    }
    for(UIView *subview in proto.subviews) {
        [self addSubview:subview];
    }
    [self addConstraints:constraints];
}

これを使用して、次のMyCustomViewProtoように宣言できます。

@interface MyCustomViewProto : MyCustomView
@end

@implementation MyCustomViewProto
- (id)awakeAfterUsingCoder:(NSCoder *)aDecoder {
    MyCustomView *view = [MyCustomView loadFromNib];
    [view copyPropertiesFromPrototype:self];

    // copy additional properties as you like.

    return view;
}
@end

XIB:

XIBスクリーンショット

ストーリーボード:

ストーリーボード

結果:

ここに画像の説明を入力してください


3
解決策は最初の問題よりも複雑です。再帰ループを停止するには、コンテンツビューをタイプMyCustomViewクラスタイプとして宣言するのではなく、ファイルの所有者オブジェクトを設定する必要があります。
ボグダンオヌ2014年

これは、a)単純な初期化プロセスであるがビュー階層が複雑であり、b)初期化プロセスが複雑であるがビュー階層が単純であるというトレードオフにすぎません。n'est-ce pas?;)
rintaro 2014年

このプロジェクトのダウンロードリンクはありますか?
karthikeyan 2015

13

忘れないでください

2つの重要なポイント:

  1. .xibのファイルの所有者をカスタムビューのクラス名に設定します。
  2. .xibのルートビューのカスタムクラス名をIBに設定しないでください

再利用可能なビューの作成を学びながら、このQ&Aページに何度かアクセスしました。上記の点を忘れると、無限再帰が発生する原因を突き止めるのに多くの時間を浪費しました。これらの点は、ここや他の場所で他の回答に記載されていますが、ここで強調したいと思います。

手順を含む私の完全なSwiftの答えはここにあります


2

上記のソリューションよりもはるかにクリーンなソリューションがあります:https//www.youtube.com/watch?v = xP7YvdlnHfA

ランタイムプロパティはなく、再帰呼び出しの問題もまったくありません。試してみたところ、ストーリーボードとXIBからIBOutletプロパティ(iOS8.1、XCode6)を使用してチャームのように機能しました。

コーディングを頑張ってください!


1
ありがとう@ingaham!ただし、ビデオで概説されているアプローチは、元の質問で提案された2番目のソリューションと同じです(Swiftコードは上記の私の回答に示されています)。どちらの場合も同様に、ラッパーUIViewサブクラスにサブビューを追加する必要があります。そのため、再帰呼び出しに問題はなく、ランタイムプロパティやその他の複雑なものに依存する必要もありません。前述のように、欠点は、冗長な追加の子ビューをカスタムUIViewクラスに追加する必要があることです。しかし、議論したように、これはあなたが言うように今のところ最良で最も単純な解決策かもしれません。
ケンチャットフィールド2015年

はい、あなたは完全に正しいです、これらは同一の解決策です。ただし、冗長なビューが必要ですが、これは最もクリーンで保守しやすいソリューションです。そこで、これを使うことにしました。
インガハム2015年

冗長な見方は完全に自然であり、他の解決策はあり得ないと私は信じています。「「何か」は「ここ」に行く」と言っていることに注意してください...「ここ」は自然に存在するものです。そこには単に「何か」、「物を置く場所」、つまり「ビュー」とは何かという定義がなければなりません。そして待って!Appleの「コンテナビュー」は、確かに、まさにそれです..何かをセグエする「フレーム」、「ホルダービュー」(「コンテナビュー」)があります。実際、「冗長」ビューソリューションは、正確には、手作りのコンテナビューです。Appleのものを使うだけです。
Fattie 2015年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.