SDWebImage や AFNetworking でも使われている、Category でもプロパティを持つ方法

More than 1 year has passed since last update.

Why?

既存のクラスでのカテゴリで様々な処理を実装したり、大きい実装をカテゴリで分割したいときがあります。前者は特に UIKit を拡張することが多いかと思います。

しかし、カテゴリではプロパティを持てません。
このような場合に、プロパティの保持が必要な処理を実装したいときどうするか。

半分黒魔術で、カテゴリでもプロパティを保持する方法があります。

ちなみに SDWebImage や AFNetworking では UIImageView と画像を取得する Operation を関連づける為に使っています。
- setImageWithURL: 等使ったことがあれば、この黒魔術で支えられた実装は、日常的に経験していると思います。(なので敬遠するは必要なく、設計上合理的であれば使えるものは使うと良いと思います。)

How

objc/runtime.hobjc_setAssociatedObject, objc_getAssociatedObject を使います。
詳細はさておき、このメソッドを使えば Key-Value で好きなオブジェクトにオブジェクトを保持させることが出来ます。

使い方は簡単で、カテゴリで @property で普通に宣言し、Getter / Setter の実装をこれらのメソッドを使って書き換えます。

次のように、実装するカテゴリと同じファイルにプロパティ保持用のカテゴリを別に作るのがオススメです。

#import <objc/runtime.h>

# pragma mark - Category for Retaining Properties

@interface UIImage (SomeCategoryProperty)

@property (nonatomic, strong) NSString *sc_someProperty;

@end

@implementation UIImage (SomeCategoryProperty)

- (NSString *)sc_someProperty {
    return objc_getAssociatedObject(self, @selector(sc_someProperty)); // #1
}

- (void)setSc_someProperty:(NSString *)sc_someProperty {
    objc_setAssociatedObject(self, @selector(sc_someProperty), sc_someProperty, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

# pragma mark - Category Implementation

@interface UIImage (SomeCategory)

@end

@implementation UIImage (SomeCategory)

// ここで self.sc_someProperty を使うことが出来ます。

@end

多少実装部が複雑に見えますがテンプレです。dispatch_once でのシングルトン実装のようなものだと思いましょう。

Advantages?

  1. UIKit での少々複雑な拡張でも、サブクラスではなくカテゴリで出来る (#2)
  2. 多少大きいクラスを実装する際、関連するメソッドをまとめたカテゴリ群で実装を構成する場合があります。この際に大活躍。 (#3)

所詮テクニックなので大きく何かに寄与するということはありません。好みです。

Disadvantage としては、@selector() はリファクタリングが効かないので @property をリファクタリングした際に見落とす可能性があります。
単純なテストを書いておけばこれは発見可能です。

補遺

  • #1 第二引数は @selector(propertyKey) で指定すると補完も効くし、ついでに Getter / Setter の宣言もしてくれるしでおすすめです。
  • #2 UIKit で例えば「○○ なラベル」を作る際に安易にサブクラス化するのは、一般的に、拡張性を考えると避けた方が良いです。(○○ で △△ なラベル…と拡張していくときに困る。)カテゴリで済むものはカテゴリにした方が良い。詳細は iOS UI Component API Design を参照してください。
  • #3 クラスを Core 実装 + 複数個のカテゴリで実装すると、1.#import でメソッドの依存関係が明白になる 2.プロパティのスコープを狭められる(原則同一カテゴリ内のみ)といった利点があります。

参考 URL