継承を使わずにクラスにプロパティを設定する方法!

f:id:vasilyjp:20180927090637j:plain

こんにちは。 年明けから自転車でずっこけて頬骨を骨折→入院→手術と迷惑かけまくったiOSエンジニアの庄司です。 最近、Objective-Cのオープンソースのライブラリを読んでいて、気になった機能があり、実際につかてみて便利だったので紹介します。

概要

クラスのカテゴリ機能を使うことで、既存クラスにメソッドを追加することはできますが、インスタンス変数を追加することはできません。 「関連参照(技術書によっては「連想参照」とも言われています。)」というテクニックを使うと『あるオブジェクトに対して別のオブジェクトの追加する』ことができます。 この機能とカテゴリを組み合わせることで、継承機能を使わずにクラス定義を柔軟に拡張することができます。 通常、クラスのインスタンス変数の定義は、インターフェース部にインスタンス変数の宣言を記述すると、そのクラスのすべてのインスタンスがその変数を持つようになります。 関連参照では、ランタイムシステムの機能を使って「オブジェクトへの参照をくっつける」「そのオブジェクトへの参照をくっつけて増やす」ことで擬似的にインスタンス変数と同じ機能を提供できます。

メリット

複数プロジェクトで使う機能や、オープンソースとして公開するときなどに、関連参照でクラスを拡張したクラスをインポートすることで、既存のソースコードの修正を最小限に抑えられます。 逆に、外部のライブラリを使っていて、コードを編集できない場合にクラスを拡張するのに使えると思います。

実装方法

オブジェクトの関連の設定、取得は以下のように書きます。ヘッダファイルは objc/runtime.h です。

オブジェクトの参照の関連付け (プロパティのセッタメソッドになる)

// id object: プロパティを持たせたいオブジェクト
// void *key: プロパティのアドレス (static で宣言した不変なローカルな静的変数を指定する)
// id value: 保持させるのオブジェクト
// objc_AssociationPolicy policy: 関連付けの方法 (後述)
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)

関連付けたオブジェクトの参照 (プロパティのゲッタメソッドになる)

// id object: プロパティを保持するオブジェクト
// void *key: プロパティのアドレス (static で宣言した不変なローカルな静的変数を指定する)
id objc_getAssociatedObject(id object, const void *key)

objc_AssociationPolicy

objc_AssociationPolicyは、参照オブジェクトがどのように保持されるかを指定します。 クラスのプロパティ宣言時と同じように設定すれば良いですが、現時点ではARCではweakに当たる設定がありません。 以下の5種類があります

/* objc_setAssociatedObject() options */
enum {
    OBJC_ASSOCIATION_ASSIGN = 0, // assign
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, // retain, nonatomic (ARC時はstrong, nonatomicと同意)
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3, // copy, nonatomic
    OBJC_ASSOCIATION_RETAIN = 01401, // retain OBJC_ASSOCIATION_COPY = 01403 // copy
};
typedef uintptr_t objc_AssociationPolicy;

詳細はMac Developer Libraryに記載されています。

実装例

UIImageViewをたくさん使っているコードの中で、画像にタップイベントを持たせたいときに、UIImageViewUIButtonに置き換えて、画像を設定して、タップイベントのメソッドを設定して……とやっていると面倒です。 関連参照を使えば、UIImagevViewにBlocksのプロパティを持たせるようにして、実装できます。

カテゴリでプロパティの定義

#import <UIKit/UIKit.h>
typedef void (^basicBlock)(void);

@interface UIImageView (Action)
@property (copy, nonatomic) basicBlock imageViewPressedBlock;
@end

UIImageViewオブジェクトにタップ時のブロック処理を設定

#import "UIImageView+Action.h"
#import <objc/runtime.h>

static char kImageViewPressedBlockKey; // 一意に決まって変更されないアドレスを定義

@implementation UIImageView (Action)
@dynamic imageViewPressedBlock; // アクセサは自分で定義する

// プロパティのセッタメソッド
- (void)setImageViewPressedBlock:(basicBlock)imageViewPressedBlock {
    objc_setAssociatedObject(self, // UIImageViewインスタンス(=self)にプロパティを持たせる
                                                 &kImageViewPressedBlockKey, // 保持するオブジェクトのアドレスを
                                                 imageViewPressedBlock, // 引数のBlocksオブジェクトをプロパティとして保時
                                                 OBJC_ASSOCIATION_COPY_NONATOMIC // Blocksオブジェクトはcopy指定
    );
}

// プロパティのゲッタメソッド
- (basicBlock)imageViewPressedBlock {
    return objc_getAssociatedObject(self, // UIImageViewインスタンス(=self)が保持するデータを取り出す
                                                             &kImageViewPressedBlockKey // 保持されたオブジェクトのアドレス
    );
}

// 画像にタッチイベントの設定
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];

    if (self.imageViewPressedBlock) {
        self.imageViewPressedBlock();
    }
}

@end

↓実行結果のシミュレータのキャプチャ capture_1

このUIImageView+Action は別の既存プロジェクトに持って行っても、以下の2点だけで使えるようになります。 1. UIImageView+Action.h をインポート。 2. UIImageViewオブジェクトにimageViewPressedBlockを定義。 わざわざ各プロジェクトで拡張したクラスを用意する必要はありません。

iQONでの実装

iQONのアプリでは、コーディネートで使われている各アイテムをタップするとアイテムの詳細情報がポップアップするようになっています。 このポップアップの実装は、上の関連参照の実装例に少し手を加えています。 1. どのアイテムがタップされたかヒットテストを行う 2. タップされたアイテムのIDをBlocksに渡して、アイテムの詳細情報を取得してポップアップを表示する。

capture_2 capture_3

まとめ

継承を使わずにクラスにプロパティを保持する方法を紹介しました。 RubyやJavaScriptなど、他の言語にもあるような便利な機能ですが、他の言語と同様、既存クラスの拡張には注意が必要です。 追加するプロパティが多くなるようであれば、コード管理の混乱を避けるために通常通りクラスの継承を行うべきだと思います。

カテゴリー