Xibで作成したカスタムビューをUIStoryboardに置いてそれが、ちゃんと表示されたらいいですよね。ちゃんと、Autolayoutにも対応したりして。で、更にその同じXibで作成したクラスをコードからも使えたらいいですよね、ってことで前から書きたいと思っていたのですが、やってみたので書いてみます。
そうそう、これに着いてリンクを教えて頂いた方ありがとうございました!
と言っても先駆者はいるので、先駆者のやり方を参考にした上で組み合わせて自分なりのカスタムビューを作ってみました。
先駆者の記事
- Reusing views in storyboards with Auto Layout — Cocoanuts iOS Blog
- よく考えられています。UIStoryboardの分離についても学ぶところがあります。Xibで作成したUIViewの作り方については2種類考案されていますが、後半のシンプルな方を今回は採用しています。
- Auto LayoutでCustom Viewを作る その1
- 非常にわかりやすく参考になりました。
Xibで作ったビューをUIStoryboard上に配置して使いまわす
まずは先駆者の記事の1番めを参考に、コードを書いていきます。その前に使い回しクラスとXibを作ります。
こんな感じで、作りました。XibはUIViewのクラス名と同名にします。自分規約です。こうすることで後ほど説明するコードが汎用的に使えるようになります。
Xibをいじります。まず、File’s OwnerにReusableViewをセットします。
そして、UIViewのオブジェクトを一番下において、その上に好きなようにUIViewを配置していきます。
こんなのを作ってみました。一番下はUIView。その上にUIImageが3つ乗っています。左上のAutolayout的にはこうなっています。
他のものも線で簡単に説明するとこうなります。
コードを書いていきます。ReusableView.mを開いて下記のように書きます。
#import "ReusableView.h" @implementation ReusableView - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { NSBundle *mainBundle = [NSBundle mainBundle]; NSArray *loadedViews = [mainBundle loadNibNamed:NSStringFromClass(self.class) owner:self options:nil]; UIView *loadedSubview = [loadedViews firstObject]; [self addSubview:loadedSubview]; loadedSubview.translatesAutoresizingMaskIntoConstraints = NO; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeTop]]; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeLeft]]; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeBottom]]; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeRight]]; } return self; } - (NSLayoutConstraint *)pin:(id)item attribute:(NSLayoutAttribute)attribute { return [NSLayoutConstraint constraintWithItem:self attribute:attribute relatedBy:NSLayoutRelationEqual toItem:item attribute:attribute multiplier:1.0 constant:0.0]; } @end
はい。準備は出来ました。このReusableViewをUIStoryboard上で扱ってみたいと思います。まず、とりあえずProjectをSingleViewで作成したので、最初のUIViewControllerに適当なサイズのUIViewを置きます。わかりやすいように緑色にしました。Autolayoutはちょっと見にくいですが先ほどと色を揃えて黄色で書いています。
この緑色のUIViewのクラスを先ほどのReusableViewに変更しておきます。この段階で、一旦Doneです。XcodeをRunしてみます。
変な顔。横にしてみます。
ちゃんとAutolayoutも適用されているのがわかります。
さて、このビューの構造をRevealで見てみます。
お分かりでしょうか?ReusableViewで指定したはずの緑色のビューが残っているように見えます。階層がひとつ余分にあるように思えます。これはその通りで、UIStoryboardでReusableViewにクラス指定したビューはインスタンス化される際に、Xibで作成したカスタムのビュー構造を読み込みません(デフォルトの振る舞いです)。インスタンス化される際に、読み込まないのですが、その後、initWithCoder:
メソッドの中で、XibからViewのヒエラルキーをインスタンス化してその上に乗せるような形に実装しています。こういう構造なので一番下の緑色のViewを外すことはできません。
本当はこの緑色の部分がなくなってほしいのですが、これは健康的に諦めたほうが良さそうです(先駆者1の記事参照)。この部分をなくすこともできますが複雑な処理を挟む必要があり、将来的に問題を起こす可能性があると思うからです。
さて、後半ちょっと面倒な説明がありましたが、とりあえずXibで作ったViewをシンプルな構造を維持したままUIStoryboardで使い回しができるようになった!やった!ということにします。
次は、これをコードでも書きたい。というケースの説明をします。
先ほどのビューをコードでも使いたい
上述のコードを少し拡張します。ここは先駆者2の記事を参考にしています。
#import "ReusableView.h" @implementation ReusableView - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (void)commonInit { NSBundle *mainBundle = [NSBundle mainBundle]; NSArray *loadedViews = [mainBundle loadNibNamed:NSStringFromClass(self.class) owner:self options:nil]; UIView *loadedSubview = [loadedViews firstObject]; [self addSubview:loadedSubview]; loadedSubview.translatesAutoresizingMaskIntoConstraints = NO; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeTop]]; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeLeft]]; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeBottom]]; [self addConstraint:[self pin:loadedSubview attribute:NSLayoutAttributeRight]]; } - (NSLayoutConstraint *)pin:(id)item attribute:(NSLayoutAttribute)attribute { return [NSLayoutConstraint constraintWithItem:self attribute:attribute relatedBy:NSLayoutRelationEqual toItem:item attribute:attribute multiplier:1.0 constant:0.0]; } @end
こんな感じになります。コードを分離するために新たにCodeViewControllerというものを作りました。CodeViewControllerのコードはこのような感じになります。Autolayoutのところ変かもしれませんが、とりあえずコードでも書けることがわかりました。
#import "CodeViewController.h" #import "ReusableView.h" @interface CodeViewController () @end @implementation CodeViewController - (void)viewDidLoad { [super viewDidLoad]; ReusableView *rView = [[ReusableView alloc] initWithFrame:CGRectZero]; [self.view addSubview:rView]; [rView setTranslatesAutoresizingMaskIntoConstraints:NO]; [self.view addConstraint:[NSLayoutConstraint constraintWithItem:rView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.topLayoutGuide attribute:NSLayoutAttributeBottom multiplier:1.0 constant:80.0]]; [self.view addConstraint:[NSLayoutConstraint constraintWithItem:rView attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeft multiplier:1.0 constant:100.0]]; [self.view addConstraint:[NSLayoutConstraint constraintWithItem:rView attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeRight multiplier:1.0 constant:-100.0]]; [self.view addConstraint:[NSLayoutConstraint constraintWithItem:rView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.bottomLayoutGuide attribute:NSLayoutAttributeTop multiplier:1.0 constant:-80.0]]; } @end
Github
コードはGithubにアップしておきます。将来的にパスが変更になったらごめんなさい。