職人が教える!iOSアプリ開発で使いこなしたいとっておきのOSS

第3回実装の面倒な部分をうまくラップしてくれている便利カテゴリ3選

Objective-Cのカテゴリの仕組みを使うと、既存クラスに手を加えることなく機能を追加することができます。たとえば、NSArrayで配列内の要素をシャッフルしたい場合、ヘッダファイルを作成してシャッフル用メソッドを定義し、

@interface NSArray (Shuffle)
- (NSArray *)shuffle;
@end

下記のように実装しておけば、

@implementation NSArray (Shuffle)

- (NSArray *)shuffle {
    // NSArray の要素をシャッフルするコード
}

@end

あとはヘッダファイルをimportするだけでshuffleというメソッドがNSArrayのオブジェクトから使用できるようになります。

このように、カテゴリという仕組みを使用すると使い回しのしやすい形で既存クラスを機能拡張できるため、UIKitやFoundationフレームワークで提供されている頻出クラスには多くのカテゴリがオープンソースで公開されています。

今回はその中でも実装の面倒な部分をうまくラップしてくれているものを中心に3つほどカテゴリを紹介します。

UIImageに各種画像処理機能を持たせるNYXImagesKit

UIImageのカテゴリといえばリサイズ、回転、角丸、マスクあたりを一番多く見かけるのですが、このNYXImagesKitはそのあたりの頻出事項はもちろん、ガウシアンによるブラー、赤目補正、輝度補正、コントラスト調整、エッジ検出、エンボス効果、ガンマ補正、セピア、先鋭化……等々の、各種画像処理メソッドが追加されています。

-(UIImage*)gaussianBlurWithBias:(NSInteger)bias;
-(UIImage*)redEyeCorrection;
-(UIImage*)brightenWithValue:(float)factor;
-(UIImage*)contrastAdjustmentWithValue:(float)value;
-(UIImage*)edgeDetectionWithBias:(NSInteger)bias;
-(UIImage*)embossWithBias:(NSInteger)bias;
-(UIImage*)gammaCorrectionWithValue:(float)value;
-(UIImage*)grayscale;
-(UIImage*)invert;
-(UIImage*)opacity:(float)value;
-(UIImage*)sepia;
-(UIImage*)sharpenWithBias:(NSInteger)bias;
-(UIImage*)unsharpenWithBias:(NSInteger)bias;

各ヘッダからメソッド定義の一部を抜粋

中でもブラーやエンボス等の畳み込み演算を伴う処理では、vImageが使用可能な場合(iOS 5以降)はそちらを使用し、使用できない場合はvDSPを使用しており、速度にもこだわった実装がされています。

/// vImage (iOS 5)
if ((&vImageConvolveWithBias_ARGB8888))
{
    const size_t n = sizeof(UInt8) * width * height * 4;
    void* outt = malloc(n);
    vImage_Buffer src = {data, height, width, bytesPerRow};
    vImage_Buffer dest = {outt, height, width, bytesPerRow};
    vImageConvolveWithBias_ARGB8888(&src, &dest, NULL, 0, 0, __s_gaussianblur_kernel_5x5, 5, 5, 256/*divisor*/, bias, NULL, kvImageCopyInPlace);
    memcpy(data, outt, n);
    free(outt);
}
else
{
    const size_t pixelsCount = width * height;
    const size_t n = sizeof(float) * pixelsCount;
    float* dataAsFloat = malloc(n);
    float* resultAsFloat = malloc(n);

    /// Red components
    vDSP_vfltu8(data + 1, 4, dataAsFloat, 1, pixelsCount);
    vDSP_f5x5(dataAsFloat, height, width, __f_gaussianblur_kernel_5x5, resultAsFloat);
    vDSP_vfixu8(resultAsFloat, 1, data + 1, 4, pixelsCount);

    /// Green components
    vDSP_vfltu8(data + 2, 4, dataAsFloat, 1, pixelsCount);
    vDSP_f5x5(dataAsFloat, height, width, __f_gaussianblur_kernel_5x5, resultAsFloat);
    vDSP_vfixu8(resultAsFloat, 1, data + 2, 4, pixelsCount);

    /// Blue components
    vDSP_vfltu8(data + 3, 4, dataAsFloat, 1, pixelsCount);
    vDSP_f5x5(dataAsFloat, height, width, __f_gaussianblur_kernel_5x5, resultAsFloat);
    vDSP_vfixu8(resultAsFloat, 1, data + 3, 4, pixelsCount);

    free(resultAsFloat);
    free(dataAsFloat);
}

vImage、vDSPとはAccelerate.framework内のライブラリで、Accelerate.frameworkについてはAppleのDeveloperサイトで次のように紹介されています。

Accelerateフレームワークは、iPhoneやiPod touch用に最適化された何百もの数学関数を提供します。信号処理ルーチン、高速フーリエ変換、基本的なベクトルと行列演算、およびマトリクス因数分解や連立一次方程式の解を求めるための業界標準機能などを含みます。iOSベースのデバイスに存在する異なるハードウェアコンフィギュレーション向けに最適化されたAccelerateフレームワークにより、すべてのデバイスに効率的に動作するコードを書くことができます。

Features - iOS Technology Overview - Apple Developer

つまり簡単に言うと、iOSベースデバイスのハードウェア向けに最適化されていて高速な数学演算ライブラリ、ということになります。

その中のvImageは畳み込みや幾何変換、ヒストグラム計算などの画像処理系の関数をまとめたもので、iOS 5から利用できるようになりました。

また vDSP にはベクタ演算や行列演算、フーリエ変換等の関数が用意されており、iOS 4からサポートされています。

ここで紹介しているブラー等の画像処理で必要な畳み込み演算では、フィルタのカーネルサイズとオリジナル画像のサイズに応じて処理量が指数関数的に増大するので、この実装のおかげで実用性が高くなっています。

ちなみにWWDC2011の⁠Inside the Accelerate Framework for iOS⁠セッションのプレゼンテーション資料によると、vImageを使用しないコードによる畳み込み処理と、vImageの関数を用いた畳み込み処理とで速度を比較した場合、カーネルサイズ7×7、1024×768(iPad2の解像度)の画像に対しての処理速度はvImageを使用したほうが14倍以上も速く、なおかつ電力消費量は約9分の1に抑えられたそうです(モバイルデバイス向けであるiOSにとっては消費電力の抑制もメリットとして大きいのではないでしょうか⁠⁠。

その他、輝度補正やコントラスト調整、反転処理など、ピクセルごとの処理が必要な処理についても、高速化の観点から、Accelerateフレームワークを使用してビットマップデータの処理が行うよう実装されています。

const size_t pixelsCount = width * height;
float* dataAsFloat = (float*)malloc(sizeof(float) * pixelsCount);
float min = (float)kNyxMinPixelComponentValue, max = (float)kNyxMaxPixelComponentValue;
UInt8* dataRed = data + 1;

// 中略

vDSP_vfltu8(dataRed, 4, dataAsFloat, 1, pixelsCount);
vDSP_vsmsa(dataAsFloat, 1, &__negativeMultiplier, &max, dataAsFloat, 1, pixelsCount);
vDSP_vclip(dataAsFloat, 1, &min, &max, dataAsFloat, 1, pixelsCount);
vDSP_vfixu8(dataAsFloat, 1, dataRed, 4, pixelsCount);

また、赤目補正やセピア効果にはiOS 5より追加されたCIFilterクラスが用いられています。CIFilterは他にも多くのフィルタ機能があるので、このカテゴリを参考に、CIFilterの各種機能をラップしたメソッドを追加してみるのもいいかもしれません。

NYXImagesKitはこちらからダウンロードできます(ライセンスは Simplified BSD License⁠⁠ 。

なお、使用にあたっては、下記フレームワーク/ダイナミックリンクライブラリをプロジェクトに追加する必要があります(NYXImagesKit の全てのカテゴリを使用する場合⁠⁠。

  • Accelerate.framework
  • AsetsLibrary.framework
  • CoreImage.framework
  • ImageIO.framework
  • libcommonCrypto.dylib
  • MobileCoreServices.framework

NYXImagesKitにはサンプルアプリが付随していないため、簡単なデモアプリを作成しました。下記URLよりダウンロードいただけますので、動作やコードの確認にご利用ください。

デモアプリを実行すると、下記のようにオリジナル画像と、8つの画像処理結果をご確認いただけます。

NYXImagesKitによる画像処理結果
NYXImagesKitによる画像処理結果

これらの画像処理のいずれもNYXImagesKitを使用したったの1行で実装されています。

画像処理を行いたい場合にはOpenCV等の選択肢もありますが、NYXImagesKitを使用すればiOS SDKの標準フレームワークの使用だけで済み、実装方法もシンプルでしかも高速なので、ぜひ使用を検討してみてください。

面倒なデリゲート処理を Blocks で記述できるカテゴリ

たとえば、UIAlertViewやUIActionSheetのデリゲート処理の記述を面倒に感じている方は多いのではないでしょうか。

  • プロトコルへの準拠を宣言する
  • delegate プロパティに値をセット
  • 表示処理を実装
  • デリゲートメソッドを実装

書き出してみるとこれだけのことではあるのですが、それぞれ書く場所が違い、メソッドも増えるのでやはり煩雑さは否めません。

感じるところは皆同じらしく、この UIAlertView, UIIActionSheet のデリゲート処理を Blocks で記述できるようにしたカテゴリやサブクラスがオープンソースで数多く公開されています。

たとえば UIKitCategoryAdditions というカテゴリを使用すると、上記の手順が

  • 表示処理を実装

これだけになります。表示処理自体もオブジェクトの生成や表示メソッドのコールをラップしてくれているので、下記のように1メソッドで済みます。

[UIActionSheet actionSheetWithTitle:@"Sample" 
                            message:@"Hello World" 
             destructiveButtonTitle:@"Cancel" 
                            buttons:[NSArray arrayWithObjects:@"First", @"Second", nil]
                         showInView:self.view 
                          onDismiss:^(int buttonIndex) {
                              // キャンセル以外のボタンが押された場合の処理
                              NSLog(@"%d", buttonIndex);
                          }
                           onCancel:^ {
                               // キャンセルボタンが押された場合の処理
                               NSLog(@"Cancelled");
                           }];

また、UIImagePickerControllerを用いて画像選択UIを実装する際には、UIActionSheetで画像ソースをアルバムにするかカメラにするかを選択させてからUIImagePickerControllerを表示し、UIImagePickerControllerDelegateのデリゲートメソッドで画像選択後の処理を行う、というフローが一般的によく用いられます。

この一連のフローを実装するとそこそこのステップ数になりますが、UIKitCategoryAdditionsを用いれば、1つのメソッドをコールするだけで済むようになります。

[UIActionSheet photoPickerWithTitle:@"Choose an image" 
                         showInView:self.view 
                          presentVC:self 
                      onPhotoPicked:^(UIImage* image) {
                          // 画像が選択された場合の処理
                          NSLog(@"%@", image);
                      }
                           onCancel:^ {
                               // キャンセルされた場合の処理
                               NSLog(@"Cancelled"); 
                           }];

……と、ここまでUIKitCategoryAdditionsを推しましたが、実はソース内部の実装を見るとあまり宜しくない点も見受けられます。

static DismissBlock _dismissBlock;
static CancelBlock _cancelBlock;
            
+ (void) actionSheetWithTitle:(NSString*) title                     
                      message:(NSString*) message          
       destructiveButtonTitle:(NSString*) destructiveButtonTitle
                      buttons:(NSArray*) buttonTitles
                   showInView:(UIView*) view
                    onDismiss:(DismissBlock) dismissed                   
                     onCancel:(CancelBlock) cancelled
{
    [_cancelBlock release];
    _cancelBlock  = [cancelled copy];
    
    [_dismissBlock release];
    _dismissBlock  = [dismissed copy];
    
    // 後略

このようにstatic変数にBlockをcopyして保持しており、直前でreleaseしているものの、最後に使用したBlockの参照はずっと残っていることになります。インスタンス変数に保持するか、使い終わった後にBlockを解放するかの修正を行って使用した方がベターです。

インスタンス変数に保持するようにした場合、現状クラスメソッドとなっているものをインスタンスメソッドにする必要があり、せっかくのシンプルなAPIに手を入れることになりますので、筆者は後者の方法(使い終わった後にBlockを解放)でforkしました。forkしたリポジトリのURLは下記になります。

このUIKitCategoryAdditionsだけでなく、Blockオブジェクトの保持はどのBlocks関連ライブラリにもついてまわるので、使用する際には「Blockをどのように保持しているか?」⁠ちゃんとcopyしているか、必要なくなったら解放しているか)についてご注意ください。

そして、Blocksを使用したライブラリを選ぶ際の注意点がもう一点あります。⁠循環参照」です。

Block内でそのBlockを保持するオブジェクト自身を強参照すると、循環参照になってしまいます。たとえばselfは__strong修飾子付きid型変数なので、Block内でselfを使用している場合は循環参照になっている可能性があります。そのような場合は、下記のように__weak修飾子を付けた変数に代入することで回避可能です。

id __weak hoge = self;

上で紹介したUIKitCategoryAdditionsにも、Blocksを使用したカテゴリとしては、遅延実行をBlocksで記述できるようにしたもの、CAAnimationのアニメーション終了後の処理を Blocksで記述できるようにしたもの、KVOのオブザーバの処理をBlocksで記述できるようにしたもの、NSTimer+Blocks等々、数多く便利なものが公開されているのですが、上記のようなBlocksの注意点を踏まえ現時点で最もお勧めできるのは、BlocksKitです。

有名なOSSなので利用者も多く、更新やforkも比較的頻繁に行われているため信頼性は他の類似ライブラリより高いといえます。何よりもBlocksの応用で考え得る便利な拡張機能をだいたい備えており、その名の通りBlocksを使用したライブラリの集大成のようなOSSとなっています。

日本語での解説記事もいくつかあるので本連載では解説は省略しますが、とても便利なライブラリなのでぜひお試しください。

英単語の単数形/複数形変換をメソッド1つで行えるようにするNSStringのカテゴリ

iOSアプリを英語ローカライズする際、単数形/複数形の取り扱いはなかなかやっかいです。-s, -es の違いを始めとして、不可算名詞、person/peopleのようにイレギュラーなものもあります。

それら諸々のルールをプロパティリストファイルに記述し、

- (NSString *)pluralizeString;
- (NSString *)singularizeString;

このように非常にシンプルなメソッドで単数/複数変換を行えるようにしたカテゴリがActiveSupportInflectorです。

ダウンロード先は下記になります(MITライセンス⁠⁠。

使用にあたってはNSString+ActiveSupportInflectorクラス以外に、ActiveSupportInflectorクラスとActiveSupportInflector.plistをプロジェクトに追加する必要があります。

ちなみに、上記リンク先はforkした筆者のリポジトリになるのですが、修正点としては下記のようにNSString+ActiveSupportInflector.mを1行コメントアウトしてあります。

//#import "NSString+MSAdditions.h"

このNSString+MSAdditions.hというファイルはリポジトリに存在していないのですが、そもそもこれに依存したコードになっていないので、コメントアウトすることで問題なくビルドできるようになります。

このカテゴリによってどんな変換ができるようになるかは、リポジトリ内に付随しているテストケース用のプロパティリストファイルActiveSupportInflectorTest.plistを見ると一目瞭然ですので、使用を検討されている方はチラッとのぞいてみてください。以下に一部抜粋しておきます。

("search", "searches"),
("stack", "stacks"),
("fish", "fish"),
("category", "categories"),
("ability", "abilities"),
("agency", "agencies"),
("index", "indices"),
("wife", "wives"),
("safe", "saves"),
("half", "halves"),
("person", "people"),
("man", "men"),
("woman", "women"),
("basis", "bases"),

まとめ

UIImageに各種画像処理機能を持たせるNYXImagesKit、UIAlertViewやUIActionSheetのデリゲート処理をBlocksで記述できるようにするUIKitCategoryAdditions、NSStringに単数形/複数形変換機能を持たせるActiveSupportInflectorの3つのカテゴリを紹介しました。

今回は記事の都合上3つしか紹介できませんでしたが、まだまだオープンソースの便利なカテゴリは数多く存在しますので、また別の回で紹介したいと思います。

おすすめ記事

記事・ニュース一覧