iOSでUnderscore.jsライクに処理できるUnderscore.m〜前編〜

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

Underscore.mとは?

Underscore.mはNSArray(NSMutableArray)やNSDictionary(NSMutableDictionary)に対してのデータの加工処理を容易にするためのオープンソース(MITライセンス)のライブラリです。JSでUnderscore.jsを利用していた方には馴染み深いですね。これらのデータに対する処理を実装することはよくあることですので、覚えておいて損はないと思います。

Underscore.mにあるサンプルを用いて説明すると、例えば「言語をキーとして挨拶文を定義したNSDictionaryがあり、この挨拶文に含まれる単語の先頭を大文字に変換したものを配列として取得する。ただし挨拶文が必ず定義されている訳ではない」といった処理があるとします。この場合、通常であれば以下のように記述するかと思います。

NSDictionary *dictionary = @{
  @"en": @"Hello world!",
  @"sv": @"Hej världen!",
  @"de": @"Hallo Welt!",
  @"ja": [NSNull null] // 日本語だけ挨拶文が設定されていない
};

NSMutableArray *temp = [NSMutableArray array];

for (NSString *key in dictionary.allKeys) {
    if ([dictionary[key] isKindOfClass:[NSString class]]) {
        [temp addObject:[dictionary[key] capitalizedString]];
    }
}

NSArray *capitalized = [NSArray arrayWithArray:temp];

Underscore.mを使用すると以下のように書けます。

NSArray *capitalized = Underscore.dict(dictionary)
    .values
    .filter(Underscore.isString)
    .map(^NSString *(NSString *string) {
        return [string capitalizedString];
    })
    .unwrap;

ポイントはプロトタイプチェーンが利用できることです。このように、Underscore.mを使用するとデータの加工処理を直感的に記述できるようになります。それでは早速使ってみましょう!

Underscore.mの導入

Underscore.mの導入は非常に簡単で、代表的なものとして以下の2つがあります。

CocoaPodsを利用する

CocoaPodsを利用する場合は、Podfileに以下の行を追記して「pod install」を実行するだけです。

pod 'Underscore.m'

ソースコードを直接インポートする

Underscore.mのソースコードを直接インポートするには、まずソースコードをrobb/Underscore.mからダウンロードし、以下のファイルをプロジェクトに追加すればOKです。

  • Underscore/
    • Underscore-Prefix.pch
    • Underscore.h
    • Underscore.m
    • Underscore+Functional.h
    • Underscore+Functional.m
    • USArrayWrapper.h
    • USArrayWrapper.m
    • USConstants.h
    • USDictionaryWrapper.h
    • USDictionaryWrapper.m

Underscore.mの使い方〜NSArray〜

Underscore.mではNSArrayに対して直説処理を行うのではなく、「対象となるNSArrayインスタンスをUSArrayWrapperインスタンスに変換したものに対して処理を行い、その結果をNSArrayに戻す」といった感じに使用します。

NSArrayとUSArrayWrapperの相互変換

NSArrayインスタンスをUSArrayWrapperインスタンスに変換するにはUnderscore.array(NSArray *array)を使用します。

USArrayWrapper *arrayWrapper = Underscore.array(array);

逆にUSArrayWrapperインスタンスからNSArrayへの変換はwrapper.unwrapを使用します。

NSArray *array = arrayWrapper.unwrap(arrayWrapper);

とくに以下のインスタンスメソッドを実行した場合は処理の結果としてUSArrayWrapperを返すためunwrapしてやる必要があります。

  • head
  • tail
  • flatten
  • without
  • shuffle
  • each
  • map
  • pluck
  • uniq
  • filter
  • reject

first〜先頭の値を取得する〜

要素の先頭を取得するにはUnderscore.first(NSArray *array)を使用します。配列が空の場合はnilが返ります。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
id first = Underscore.array(array).first; // @1が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
id first = Underscore.first(array); // @1が返る

last〜先頭の値を取得する〜

要素の最後を取得するにはUnderscore.last(NSArray *array)を使用します。これもfirstと同様、配列が空の場合はnilが返ります。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
id last = Underscore.array(array).last; // @7が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
id last = Underscore.last(array); // @7が返る

head〜先頭から指定した数だけ要素を取得する〜

先頭から指定した数だけ要素を取得するにはUnderscore.head(NSArray *array, NSUInteger n)を使用します。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *firstThree = Underscore.array(array).head(3).unwrap; // @[@1, @2, @3]が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *firstThree = Underscore.head(array, 3); // @[@1, @2, @3]が返る

tail〜末尾から指定した数だけ要素を取得する〜

逆に末尾から指定した数だけ要素を取得するにはUnderscore.tail(NSArray *array, NSUInteger n)を使用します。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *lastThree = Underscore.array(array).tail(3).unwrap; // @[@5, @6, @7]が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *lastThree = Underscore.tail(array, 3); // @[@5, @6, @7]が返る

indexOf〜指定した値が最初に見つかったときのインデックスを返す〜

指定した値が最初に見つかったときのインデックスを取得するにはUnderscore.indexOf(NSArray *array, id obj)を使用します。指定した値が配列に含まれていない場合はNSNotFoundが返ります。

NSArray *alphabets = @[@"a", @"b", ・・・ , @"z"];
NSInteger index = Underscore.array(alphabets).indexOf(@"z"); // 25が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *alphabets = @[@"a", @"b", ・・・ , @"z"];
NSInteger index = Underscore.indexOf(alphabets, @"z"); // 25が返る

flatten〜多次元配列を1次元配列に変換する〜

多次元配列を1次元配列に変換するにはUnderscore.flatten(NSArray *array)を使用します。

NSArray *arrayOfArrays = @[@[@1, @2], @[@3, @4], @[@[@5, @6, @7], @[@8, @9]]];
NSArray *oneToNine = Underscore.array(arrayOfArrays).flatten.unwrap; // @[@1, @2, @3, @4, @5, @6, @7, @8, @9]が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *arrayOfArrays = @[@[@1, @2], @[@3, @4], @[@[@5, @6, @7], @[@8, @9]]];
NSArray *oneToNine = Underscore.flatten(arrayOfArrays); // @[@1, @2, @3, @4, @5, @6, @7, @8, @9]が返る

without〜指定した要素を除いた配列を取得する〜

指定した要素を除いた配列を取得するにはUnderscore.without(NSArray *array, NSArray *values)を使用します。除外したい要素はNSArrayで指定します。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *evenNumbers = @[@2, @4, @6];
NSArray *oddNumbers = Underscore.array(array).without(evenNumbers).unwrap; // @[@1, @3, @5, @7]が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *evenNumbers = @[@2, @4, @6];
NSArray *oddNumbers = Underscore.without(array, evenNumbers); // @[@1, @3, @5, @7]が返る

shuffle〜配列の要素をシャッフルする〜

配列の要素をシャッフルするにはUnderscore.shuffle(NSArray *array)を使用します。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *shuffled = Underscore.array(array).shuffle.unwrap; // 要素をシャッフルした配列が返る(例:@[@6, @3, @7, @1, @4, @2, @5])

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @2, @3, @4, @5, @6, @7];
NSArray *shuffled = Underscore.shuffle(array); // 要素をシャッフルした配列が返る(例:@[@6, @3, @7, @1, @4, @2, @5])

reduce(reduceRight)〜配列を1つの値に圧縮する〜

配列を1つの値に圧縮するにはUnderscore.reduce(id memo, UnderscoreReduceBlock block)Underscore.reduceRight(id memo, UnderscoreReduceBlock block))を使用します。第1引数に指定するmemoは初期状態として使用できます。reduceが先頭から末尾に向かって処理していくのに対し、reduceRightは末尾から先頭に向かって処理していきます。

NSArray *numbers = @[@1, @2, @3, @4, @5, @6, @7];
NSNumber *sum = Underscore.array(numbers)
	.reduce(@0, ^(NSNumber *x, NSNumber *y) {
    	return @(x.integerValue + y.integerValue);
	}); // @28が返る
)

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *numbers = @[@1, @2, @3, @4, @5, @6, @7];
NSNumber *sum = Underscore.reduce(numbers, @0, ^(NSNumber *x, NSNumber *y) {
    return @(x.integerValue + y.integerValue);
});

each(arrayEach)〜配列のそれぞれの要素に処理を適用する〜

配列のそれぞれの要素に処理を適用するにはwrapper.each(UnderscoreArrayIteratorBlock block)Underscore.arrayEach(NSArray *array, UnderscoreArrayIteratorBlock block))を使用します。元の配列の変更は行われません。

NSArray *numbers = @[@1, @2, @3, @4, @5, @6, @7];
Underscore.array(numbers).each(^(id obj) {
    NSLog(@"%@", obj); // 順番に出力される
});

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *numbers = @[@1, @2, @3, @4, @5, @6, @7];
Underscore.arrayEach(numbers, ^(id obj) {
    NSLog(@"%@", obj); // 順番に出力される
});

map(arrayMap)〜配列のそれぞれの要素に処理を適用した結果から新たに配列を作成する〜

配列のそれぞれの要素に処理を適用した結果から新たに配列を作成するにはwrapper.map(UnderscoreArrayMapBlock block)Underscore.arrayMap(NSArray *array, UnderscoreArrayMapBlock block))を使用します。blockの中でnilを返すとその要素は無視されます。

NSArray *array = @[@"one", @"two", @"three"];
NSArray *capitalized = Underscore.array(array).map(^(NSString *string) {
    return string.capitalizedString;
}).unwrap; // @[@"One", @"Two", @"Three"]が返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@"one", @"two", @"three"];
NSArray *capitalized = Underscore.arrayMap(array, ^(NSString *string) {
    return string.capitalizedString;
}); // @[@"One", @"Two", @"Three"]が返る

pluck〜指定したキーパスで取得した値から配列を作成する〜

指定したキーパスで取得した値から配列を作成するにはUnderscore.pluck(NSArray *array, NSString *keyPath)を使用します。例えば以下のようにnameとisAdminを持つUserクラスがあったとします。

#import <Foundation/Foundation.h>

@interface User : NSObject

@property (strong, nonatomic) NSString *name;
@property (assign, nonatomic) BOOL isAdmin;

@end

このUserクラスのインスタンスで構成された配列から、nameを抽出して新たに配列を作成したい場合などに有効です。

// Userインスタンスを10個生成
NSMutableArray *temp = [NSMutableArray array];
for (int i = 1; i <= 10; i++) {
    User *user = [[User alloc] init];
   
    user.name = [NSString stringWithFormat:@"user%d", i];
    user.isAdmin = (i % 2 == 0);

    [temp addObject:user];
}
NSArray *users = [NSArray arrayWithArray:temp];

// @[@"user1", @"user2", ..., @"user10"]が返る
NSArray *names = Underscore.array(users).pluck(@"name").unwrap;
[/c]

<h4>クラスメソッドを使用する場合</h4>
<p>
Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。
</p>



<h4>クラスメソッドを使用する場合</h4>
<p>
Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。
</p>



<h4>クラスメソッドを使用する場合</h4>
<p>
Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。
</p>



<h4>クラスメソッドを使用する場合</h4>
<p>
Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。
</p>


NSArray *array = @[@1, @"a", @"b", @2, @3];
NSArray *numbers = Underscore.reject(array, Underscore.isNumber).unwrap; // @[@"a", @"b"]が返る

all〜それぞれの要素に処理した結果、すべての要素でYESを返すかどうかを判定する〜

それぞれの要素に処理した結果、すべての要素でYESを返すかどうかを判定するにはUnderscore.all(NSArray *array, UnderscoreTestBlock test)を使用します。

NSArray *array = @[@1, @"a", @"b", @2, @3];
BOOL onlyStrings = Underscore.array(array).all(Underscore.isString); // NOが返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @"a", @"b", @2, @3];
BOOL onlyStrings = Underscore.all(array, Underscore.isString); // NOが返る

any〜それぞれの要素に処理した結果、いずれかの要素でYESを返すかどうかを判定する〜

それぞれの要素に処理した結果、それぞれの要素に処理した結果、いずれかの要素でYESを返すかどうかを判定するにはUnderscore.any(NSArray *array, UnderscoreTestBlock test)を使用します。

NSArray *array = @[@1, @"a", @"b", @2, @3];
BOOL onlyStrings = Underscore.array(array).any(Underscore.isString); // YESが返る

クラスメソッドを使用する場合

Underscore+Functionalに定義されるクラスメソッドを使用する場合は以下のように書きます。

NSArray *array = @[@1, @"a", @"b", @2, @3];
BOOL onlyStrings = Underscore.any(array, Underscore.isString); // YESが返る

まとめ

結構長くなってきたので今回はNSArrayについてまでにしておきます。次回はNSDictionaryとヘルパーについて紹介していきたいと思います。

参考