LoginSignup
30
31

More than 5 years have passed since last update.

XIBからもコードからもカスタムViewインスタンスを生成する方法

Posted at

背景

ViewContollerから表示に関する責務を切り離すため、カスタムViewクラスを定義しViewレイヤの処理をそちらに移すということは一般的に行われている。
その際、カスタムViewクラスのインスタンスをXIBから生成したくなる。その方法はいくつかあるが、そのうちの一つが以下のようなやり方。

CustomView.m
-(id)init
{
    NSArray *topLevelViews = [[NSBundle mainBundle] loadNibNamed:@"CustomView" owner:self options:nil];
    return topLevelViews[0];
}

この方法の問題点はカスタムViewを他のXIBに埋め込むことが出来ないこと。
他のViewのSubviewにしたい場合は以下のようにする必要がある。
・親ViewのXIBにPlaceHolderView(プレーンなUIViewのみのView)を置く。
・コードからViewインスタンスを生成し、PlaceHolderViewにaddSubviewする。

この方法では以下の問題がある。
・PlaceHolderViewを使うことで余分なViewが一つ増える。それによってパフォーマンスが劣化する。
・カスタムViewのインスタンスを生成し、PlaceHolderViewにaddSubViewするコードが増える。

これ以外の方法もあるが、コードからもXIBからもインスタンスを生成できる方法は無かった。

解決策

この問題を解決するためにコードからViewインスタンスを生成でき、
XIBにも埋め込めるカスタムViewクラスを定義出来るクラスを作成した。

THLoadXibView.h
/**
* CustomViewクラスのインスタンスをXibからもコードからも生成出来るようにするクラス。
* CustomViewはこのクラスのサブクラスとして作成する。
* CustomViewクラス作成時は以下に注意すること。
* ・XIB内の一番上の階層のViewのクラスはUIViewとする。(CustomViewクラスにしない)
* ・XIB内の一番上の階層にはUIViewを一つだけ置く。
* ・XIBのFile's OwnerをCustomViewクラスにする。
*
*/
@interface THLoadXibView : UIView
// XIBのキャッシュの容量を設定する
+ (void)setXibCacheCountLimit:(NSUInteger)limit;
@end
THLoadXibView.m
@implementation THLoadXibView
// UITableViewCellのcontentViewとして使用する際など、
// ある程度の数のインスタンスを生成する場合にパフォーマンスが劣化したため
// XIBから生成したNSCoderをキャッシュしている。
static NSCache *coderOfXibCache;
+ (void)initialize {
    coderOfXibCache = [[NSCache alloc] init];
}

+ (void)setXibCacheCountLimit:(NSUInteger)limit {
    [coderOfXibCache setCountLimit:limit];
}

// initメソッドでインスタンスを生成した場合はCustomView.xibの設定を使う。
- (id)init {
    NSCoder *coder = [self createCoder];
    return [self initWithCoder:coder];
}

// initWithFrameメソッドでインスタンスを生成した場合はCustomView.xibの設定のframeのみ上書きする。
- (id)initWithFrame:(CGRect)frame {
    self = [self init];
    if (self) {
        self.frame = frame;
        [self initialize];
    }
    return self;
}

// initWithCoderメソッドでインスタンスを生成した場合は
// 最も上の階層のView(top view)の設定のみCustomViewが埋め込まれたXIB(親XIB)内での設定を使う。
- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    // initメソッドから呼ばれた場合、aDecoderにsubViewが既に含まれているため一旦削除する
    while (self.subviews.count) {
        [self.subviews[0] removeFromSuperview];
    }
    if (self) {
        CGRect frame = self.frame;
        UIView *topView = [self createViewFromXib];
        // Subviewの位置がずれるのを防ぐためtop viewとframeを合わせてからsubviewを移す。
        // top viewは親XIBで生成されたものを使用するため、CustomView.xibから生成したものは捨てる。
        self.frame = topView.frame;
        for (UIView *view in topView.subviews) {
            [self addSubview:view];
        }
        self.frame = frame;
        [self initialize];
    }
    return self;
}

- (NSCoder *)createCoder {
    // 高速化のためにキャッシュを使用する。
    if ([coderOfXibCache objectForKey:self.nibName]) {
        return [coderOfXibCache objectForKey:self.nibName];
    } else {
        NSMutableData *data = [NSMutableData data];
        NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
        [[self createViewFromXib] encodeWithCoder:archiver];
        [archiver finishEncoding];
        NSCoder *coder = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
        [coderOfXibCache setObject:coder forKey:self.nibName];
        return coder;
    }
}

- (UIView *)createViewFromXib {
    UINib *nib = [UINib nibWithNibName:self.nibName bundle:[NSBundle mainBundle]];
    NSArray *topViews = [nib instantiateWithOwner:self options:nil];
    if (topViews.count == 1) {
        return topViews[0];
    } else {
        [NSException raise:@"Invalid xib file." format:@"%@.xib has no top level view or 2 and over. Xib file must have one top level view.", self.nibName];
        abort();
    }
}

// protected
- (NSString *)nibName {
    // デフォルトではクラス名と同名のXIBファイルが読み込まれる。別のXIBを読み込ませたい場合はoverrideすること。
    return NSStringFromClass([self class]);
}

// protected
// サブクラスで初期化処理を実行したい場合にoverrideする。
// XIBからインスタンスを作った場合も、コードからインスタンスを作った場合も呼ばれる。
- (void)initialize {}
@end

メリット・デメリット

THLoadXibViewのメリット・デメリットは以下。

メリット

・カスタムViewインスタンスをコードから生成できるし、XIBの中に埋め込むことも出来る。
 親XIBからカスタムViewの最も上の階層のViewのframeやbackgroundColor等の設定が出来る。

デメリット・改善点

・カスタムViewのXIBを定義する際に、
 最も上の階層のViewのクラスをUIViewとしなければいけないのが不自然。
 出来ればFile's Ownerを空とし、最も上の階層のViewのクラスをカスタムViewクラスとしたい。
・一般的な方法と比べて処理が多いため、パフォーマンスは低下する。
 但し、私が使用した範囲では体感できるほどのパフォーマンス劣化は無かった。
・UIButton等のように、親XIBに埋め込んだ際にsubviewが見えたりsubviewの設定値を変更出来たりすると便利・・・だけど、IBが対応しないといけないので難しいですね。
・もっとすっきり書ける方法がありそう。XIBからNSCoderを生成せずに済ませる方法は無いか。
・カスタムViewにUIView以外のクラスを継承させることが出来ない。
 最も困るのはUITableViewCellを継承させられないこと。UIButton等は最も上の階層にUIViewを置き、subviewとしてUIButtonを置けばいい。
 しかし、UITableViewにCellを表示する際は最も上の階層のViewがUITableViewCellになっているViewを使う必要がある。
・ViewとXIBが1対1でない場合使いづらい。1対1でない場合、
 クラス名と異なる名前のXIBを読み込むためにカスタムViewクラスでnibNameメソッドをoverrideする必要がある。
 この方法ではコードからインスタンスを生成する場合にしかクラス名と異なる名前のXIBを読み込むことが出来ない。
 User Defined Runtime AttributesでXIBファイル名を指定する方法を試してみたが、
 読み込むXIBファイルを決定するタイミングではまだAttributesがプロパティにセットされていないためXIBファイルを切り替えることが出来なかった。

インストール

・cocoapodを使ってインストールする場合
 以下をPodfileに追加してpod installを実行する
 pod 'THLoadXibView', :git => 'https://github.com/hosokawa0825/THLoadXibView.git'
・cocoapodを使わない場合
 THLoadXibView.h, THLoadXibView.mをコピーすればOK。

ライセンス

MITです。

30
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
31