読者です 読者をやめる 読者になる 読者になる

mixi engineer blog

ミクシィ・グループで、実際に開発に携わっているエンジニア達が執筆している公式ブログです。様々なサービスの開発や運用を行っていく際に得た技術情報から採用情報まで、有益な情報を幅広く取り扱っています。

UIView拡張カテゴリによるUIコーディングの簡略化

iOS mixi Objective-C

はじめまして、佐野です。僕は2006年10月にメンバー4人でネイキッドテクノロジー社を創業し、5年間ガラケーからスマホに渡るまでモバイル関連の技術・サービス開発に携わり、去年の10月にミクシィ社にジョインし、現在はmixiのiPhoneアプリの開発に携わっております。このブログではiOSプログラミングの初級者~中級者向けに、さらなる上達の助けになるようなことを書いていきたいと思います。

Apple 製の iOS/Mac アプリの統合開発環境であるXCodeは、ver4 から GUI ベースの UI 開発ツールである Interface Builder が組み込まれ、非常に快適にUI開発ができるようになりました。mixi の iPhone/iPad アプリも基本的には各画面が IB ファイルで構成されています。静的な画面であればこれだけで済むのですが、アニメーションを多用したインタラクティブな画面を作ろうと思うと、コードによるビューの操作が必要になります。ホームフィードの詳細画面のコメントバーなどは、ガッツリとコードでアニメーションを記述しました。

さて、UIコーディングに関する参考書を見てみると、例えばあるビューを右に10px移動させるコードはこんな風に書かれています:

CGPoint center = aView.center;
center.x += 10;
aView.center = center;

Javaに慣れ親しんだ人がこのコードを見れば「え...?」となり、当然以下のように書き直そうとするでしょう:

aView.center.x += 10;

...すると謎のエラー表示が。"Expression is not assignable." などと怒られています。何がじゃ、訳分からん。正直この瞬間にiOSプログラミングをやめたくなる人は多いんじゃないでしょうか。この問題の原因は、UIView クラスの center メソッドは CGPoint という構造体を値として返すものであり、オブジェクトの参照を返しているのではないことなのですが、しかしそんなことはどうでもいい、とにかくx座標を移動させるためだけに3行を費やすのなんて耐えられない。

「いやいや、1行で書けるよ?」とこんなコードを書く人もいるでしょう:

aView.center = CGPointMake(aView.center.x + 10, aView.center.y);

いやいや、こんなのも嫌です。aView.center.y なんていう関係ないものが出てくのも嫌だし、このやり方で width の変更を変更しようとしたらもっと悲惨です:

aView.frame = CGRectMake(aView.origin.x, aView.origin.y, aView.size.width + 10, aView.size.height);

これじゃもう何をしようとしてるコードなのか一目で分かりません。という訳で僕は「こんな不条理に世界中の人たちが黙って耐えている訳がない」と色々と模索をし、あるライブラリでこんな風に書かれているコードを見つけました:

aView.centerX += 10;

これや!!! しかし UIView には centerX などというプロパティはありません。どうしたらこれができるのでしょう。これを実現するためには、Objective-C の「プロパティ」と「カテゴリ」という仕組みを理解する必要があります。

まず「プロパティ」とは、アクセッサメソッドコールの簡略記法です。例えば UIView クラスには alpha というプロパティが宣言されていますが、aView.alpha と書けばこれは [aView alpha] とゲッタメソッドの戻り値を返し、 aView.alpha =0.5 と書けばこれは [aView setAlpha: 0.5] とセッタメソッドを呼び出すようになっています。要はコンパイラがプロパティアクセスで記述したコードを、対応するアクセッサメソッドの呼び出しに置き換えているだけなのです。

自分でプロパティを実装するときには、だいたいは.hファイルにおいて 1. インスタンス変数の宣言2. @property宣言、そして.mファイルにおいて 3. @synthesize記述 という作業を行います。これは 3. によって 1. で宣言したインスタンス変数のアクセッサメソッドが自動生成されているのですが、 3. を記述しなければ自分でアクセッサメソッドを実装することもできます。ここで、プロパティアクセスはあくまでただのアクセッサメソッドコールなので、1. も書かずに 2. だけ書いて、対応するインスタンス変数のないプロパティを独自に実装するということができるのです。

次に「カテゴリ」、これはクラスの分割モジュールのようなものですが、Objective-C の面白いのは 既存のクラスに対して独自にカテゴリを宣言・定義することでそのクラスを拡張することができる ということです。以上で準備完了です、UIView クラスの拡張カテゴリを作り、centerX というプロパティを作りましょう。ここでは MyExtension というカテゴリ名にして、UIView+MyExtension.h, m というファイルを作ります。.h はこんな感じ:

@interface UIView(MyExtention)
 
@property (nonatomic, assign) CGFloat centerX;
 
@end

次に .m の実装部分では普通のクラス定義と同じように @implementation UIView (Extention) ... @end の間で対応するメソッドを実装します。まずは centerX のゲッタ:

- (CGFloat)centerX {
    return self.center.x;
}

これだけのことです。続いてセッタ:

- (void)setCenterX:(CGFloat)centerX {
    CGPoint center = self.center;
    center.x = centerX;
    self.center = center;
}

お、これは似たようなコードを上で書きましたね。そうなんです、結局中で機械にやらせていることは同じであって、我々人間に見える部分を読みやすくしてるだけのことなんです。というわけで合わせると、

#import "UIView+MyExtension.h"
 
@implementation UIView (MyExtention)
 
- (CGFloat)centerX {
    return self.center.x;
}
 
- (void)setCenterX:(CGFloat)centerX {
    CGPoint center = self.center;
    center.x = centerX;
    self.center = center;
}
 
@end

ザッツオール!あとはこれを使いたいところで UIView+MyExtension.h をインポートすれば:

aView.centerX += 10;

がちゃんと動作します。これはコンパイラによって:

[aView setCenterX: [aView centerX] + 10];

に変換されて、上で定義したメソッドが呼び出されている訳ですね。

こんな感じで、UIView の位置やサイズに関するプロパティを悉く作っちゃいましょう:

@interface UIView(MyExtention)
 
@property (nonatomic, assign) CGFloat centerX;
@property (nonatomic, assign) CGFloat centerY;
 
@property (nonatomic, assign) CGFloat origin;
@property (nonatomic, assign) CGFloat left;
@property (nonatomic, assign) CGFloat right;
@property (nonatomic, assign) CGFloat top;
@property (nonatomic, assign) CGFloat bottom;
 
@property (nonatomic, assign) CGFloat size;
@property (nonatomic, assign) CGFloat width;
@property (nonatomic, assign) CGFloat height;
 
@end

こうすれば、例えば二つのUIViewインスタンスを、底辺を揃えて横に並べるといったコードも:

bView.bottom = aView.bottom;
bView.left = aView.right;

といった感じに、とても直観的に書けます。この拡張カテゴリのヘッダを、プロジェクトファイルと一緒に生成される ***_Prefix.pch というヘッダファイルの中でインポートしておけば、そのプロジェクト内の全てのコードからこれらのプロパティが使えるようになって便利です。

それでは、次回もこういったちょっと裏技っぽいやり方でUIコーディングがずっと快適になるような方法をご紹介したいと思います。