iOSで独自Viewを作る時の5つのTips


iOSでの独自View（iOSではカスタムビューと言うらしい？）を作る時に困ったことがあったのでまとめておくことにしました。


作りたいのは、 _主にStoryBoardやxib上に配置して使う。たまにコードから生成する事もあるカスタムビュー_ です。

# 初期化に関わるメソッドの設計をしたい

カスタムビューは`UIView`、もしくはそのサブクラスを継承して作ります。
`UIView`には初期化に関わるメソッドが複数あり、インスタンス生成の仕方によって呼ばれ方が異なります。

作りたいViewはxibからでもコードからでも生成されるので、なるべく指定イニシャライザを通して初期化するように仕向けていきます。
（[指定イニシャライザについてはこちらが詳しい](http://qiita.com/naodroid/items/186cc7f8924fd62c72c3)）

## StoryBoardやxibファイルからインスタンス化される場合に呼ばれるメソッド

以下の順番でメソッドが呼ばれます。

1. `- (id)initWithCoder:`
2. `- (void)awakeFromNib`

`- (id)initWithCoder:`は、シリアライズされたインスタンスを復元するためのメソッドです。

後述する　"User Defined Runtime Attributes" で指定したプロパティの設定は1. と 2. の間に行われるため、初期値の設定は`- (id)initWithCoder:`で行うのが良さそうです。

`awakeFromNib`はnibからの読み込みが完全に終わった時に呼び出されるので、値設定以外の初期化処理などはこの段階で行うのが良さそうです。

## コードから初期化する場合に呼ばれるメソッド

1. `- (id)init` or `- (id)initWithFrame:` or クラスの指定イニシャライザ

正直、どのイニシャライザを呼んでも自由だと思います。が、クラスが定める指定イニシャライザがあればそれを呼ぶべきでしょう。

例えば`UITableViewCell`なんかは`-(id)initWithStyle:reuseIdentifier:`が指定イニシャライザになっています。

`UITableViewCell.h`の`-(id)initWithStyle:reuseIdentifier:`付近には以下のように書かれています。

```Objective-C
// Designated initializer.  If the cell can be reused, you must pass in a reuse identifier.  You should use the same reuse identifier for all cells of the same form.  
```

リファレンスにも`initWithFrame:`は書かれていません。
マニュアルやドキュメントコメントで、指定イニシャライザを使うように促すのが良さそうです。

# StoryBoardやxibへの追加の仕方

基本的には、カスタムビューの直接の親クラスを画面に追加し、"Custom Class"の部分にカスタムビューのクラス名を記入します。
こうすることで、親クラスのプロパティ設定をInterface Builder上で行えて便利です。

## カスタムビューに持たせた独自のプロパティもIB上から設定したい

Identity Inspectorにある "User Defined Runtime Attributes" に、プロパティのパスと型、値を指定することでIB上からでも指定ができます。

![スクリーンショット 2014-09-13 13.40.19.png](https://qiita-image-store.s3.amazonaws.com/0/41896/01a3d273-6c78-8f57-d07a-e99905b0e00c.png "スクリーンショット 2014-09-13 13.40.19.png")

キー値コーディングができるので、「プロパティに入っているインスタンス」のプロパティに対して設定を行うこともできます。

値は[`[NSObject(NSKeyValueCoding) setValue:forKey:]`](https://developer.apple.com/library/mac/documentation/cocoa/reference/foundation/Protocols/NSKeyValueCoding_Protocol/Reference/Reference.html#//apple_ref/occ/instm/NSObject/setValue:forKey:)を使ってセットされているようです。
パスが見つからない場合などは例外が出ます。

"User Defined Runtime Attributes" に書いた値の設定は、`initWithCoder:`と`awakeFromNib`の間に行われるようです。

設定できる型の種類は以下

|IB上の表示|実際の型|備考|
|:---------:|:--------:|:----|
|Boolean|BOOL||
|Number|NSNumber *|CGFloat型の値が欲しいときは`[NSNumber floatValue]`とかを使う|
|String|NSSting *||
|Localized String|NSString *|[Mac OS Xのアプリでしか有効にならない模様](http://stackoverflow.com/a/24527990)|
|Point|CGPoint||
|Size|CGSize||
|Rect|CGRect|記法は{{(Point)}, {(Size)}}|
|Range|NSRange||
|Color|UIColor *||
|Image|UIImage *||
|Nil|(nil)|多分NSNullが入る？|

この機能を使う事を念頭に置いてAPI設計を行うと良さそうです。

# 画面描画したい

基本的に`- (void)drawRect:`メソッドをオーバーライドして、その中でCoreGraphics系の関数を駆使して画面を書いていきます。

描画にはグラフィクスコンテキストが必要です。

```Objective-C
CGContextRef context = UIGraphicsGetCurrentContext();
```

もう定型句と化しているので、最初からコメントアウトして置いておいてくれれば良いのにと思います。

## 背景色を設定していないのに背景が黒くなる

指定範囲に描かれたものをなかったことにする`CGContextClearRect()`を使って、初めにまっさらにしておきます。

```Objective-C
CGContextClearRect(context, rect);
```



## `UIImageView`を継承したら`drawRect:`が呼び出されない

`UIImageView`は例外的に、画面描画に`drawRect:`メソッドが使われません。（[参考: drawRect not being called in my subclass of UIImageView](http://stackoverflow.com/a/2612185)）

そのため、`UIImageView`を継承したクラスでカスタムビューを作るのは諦めた方が良いでしょう。

画面に`UIImage`を書き出したいのなら、カスタムビューの`drawRect:`の中で[`UIImage#drawInRect:`](https://developer.apple.com/library/ios/documentation/uikit/reference/UIImage_Class/Reference/Reference.html#//apple_ref/occ/instm/UIImage/drawInRect:)を呼び出す事で実現できます。
（[参考: UIImageを画面に直接描画する方法](http://lab.dolice.net/blog/2013/07/25/objc-ui-image-draw-in-rect/)）

## 影を描画したい

`CGContextSetShadow()`を使って影を設定すると、塗りつぶし等の描画を行ったときに自動的に影を付けてくれます。

ただ、塗りつぶしの度に影が描画されるため、単純な図形を組み合わせて多角形を作ろうとすると悲しいことになります。
描いた後のものに影を付けることはできないようなので、おとなしくパスを使って多角形を描画しましょう。

## UILabelを継承してるクラスで文字描画をしたい

`self.text`に文字が入っているので`[NSString drawInRect:withFont:lineBreakMode:alignment:]`を使って描画する方法もありますが、

```Objective-C
[super drawRect:rect];
```

で描いた方がはるかに楽です。


# xibで作ったビューを、独自ビューの見た目にしたい

どうやら、あまりオススメされる方法ではないらしいです。が、一応。

[`UITableViewCell`のカスタムビューをxibで作って使う方法](https://github.com/mixi-inc/iOSTraining/wiki/4.3-セルのカスタマイズ)はTableViewControllerが提供してくれていますが、それ以外の場合はそうもいきません。
それなりに面倒な手順が必要です。

流れは以下のような感じ。

1. カスタムビューのクラスを作る
2. xibで見た目を作り、File's Ownerにカスタムビューのクラスを設定
3. カスタムビューの初期化時にxibを読み込み、子ビューに設定する


## 1. カスタムビューのクラスを作る

`UIView`を継承したクラスを作ります。
例えば "SampleView" という名前にしておきます。


## 2. xibで見た目を作り、File's Ownerにカスタムビューのクラスを設定

カスタムビュークラスと同名のxibファイルを用意して適当に見た目を作ります。

このとき、xibのFile's Ownerに 1. で作ったカスタムビュークラスを設定します。

![スクリーンショット 2014-09-13 14.50.08.png](https://qiita-image-store.s3.amazonaws.com/0/41896/fb7c14b5-2ea3-fd6e-3f19-d0071606c6c5.png "スクリーンショット 2014-09-13 14.50.08.png")

ついでに、ルートビューへのアウトレットをFile's Ownerに張っておきます。

![XVAAAView_xib_—_Edited.png](https://qiita-image-store.s3.amazonaws.com/0/41896/1cb39d23-ed2f-1b26-a966-62b2e9f2624c.png "XVAAAView_xib_—_Edited.png")

名前は適当で構いません。ルートビューを後で使うためにアウトレットを作っておいた方が便利なので作成します。
ここでは`contentView`という名前にしました。

## 3. カスタムビューの初期化時にxibを読み込む

`awakeFromNib`のタイミングでxibを読み込みます。
ここでは直接`awakeFromNib`に書き込んでしまいましたが、コードからイニシャライズされた時には`awakeFromNib`が呼び出されないので、別の関数に切り出して指定イニシャライザと共有するのが良さそうです。

```Objective-C
- (void) awakeFromNib
{
    [[NSBundle mainBundle] loadNibNamed:NSStringFromClass([self class]) owner:self options:nil];
    [self addSubview:self.contentView];
}
```

これで、カスタムビューを画面に配置するとxibで作ったビューが表示されます。

# プロパティが変更された時にビューを再描画したい

Objective-Cで開発している場合は、ヘッダにプロパティを宣言すると自動的にセッタ・ゲッタのメソッドが作られます。
セッタとゲッタは`(インスタンス).(プロパティ)`の書式で呼び出された時に使われます。

```Objective-C
// これはゲッタが使われる
NSString *str = self.hoge;

// 実はこう書くのと同じ
// NSString *str = [self hoge];


// これはゲッタが使われない（@synthesizeを使っていないとき）
// ただしアンダースコアを使った記法が使えるのは当該クラスの中のみ
str = _hoge;


// これはセッタが使われる
self.hoge = @"fuga";

// こう書いても同じことが起きる
// [self setHoge:@"fuga"];


// これはセッタが使われない（@synthesizeを使っていないとき）
_hoge = @"fuga";
```

ということで、セッタをオーバーライドしてビュー再描画の命令`[UIView setNeedsDisplay]`を挟めば、プロパティが変更されたときにビューを再描画できます。

```Objective-C
- (void)setHoge:(NSString *)hoge
{
	_hoge = hoge;
	[self setNeedsDisplay];
}
```