こんな書き方できたんだ!?

今日貯めてた Reading List を見てたらこんな記事が。

New thing I do in code

以下のようなコードが動きます。

self.downloadButton = ({
    UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    NSLog(@"%s %@", __PRETTY_FUNCTION__, button);
    button.frame = CGRectMake(40, 40, 100, 50);
    [button setTitle:@"Donwload" forState:UIControlStateNormal];
    [button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    button; 
});

ここでボタンにセットしたアクションのログ(sender と self.downloadButton を出力) を見ても、生成したボタンが self.downloadButton に代入されていることが分かります。

2013-08-20 21:26:22.062 SampleApp[60019:c07] -[TestViewController viewDidLoad] <UIRoundedRectButton: 0x1101fc10; frame = (0 0; 0 0); opaque = NO; layer = <CALayer: 0x1101fce0>>
2013-08-20 21:26:24.007 SampleApp[60019:c07] -[TestViewController buttonAction:] <UIRoundedRectButton: 0x1101fc10; frame = (40 40; 100 50); opaque = NO; layer = <CALayer: 0x1101fce0>>
2013-08-20 21:26:24.007 SampleApp[60019:c07] -[TestViewController buttonAction:] <UIRoundedRectButton: 0x1101fc10; frame = (40 40; 100 50); opaque = NO; layer = <CALayer: 0x1101fce0>>

GCC の拡張で丸括弧で囲われたの中括弧内の記述が評価されるようです。仕様は以下のページに書かれています。

Statements and Declarations in Expressions

利点

さきの記事でも書かれていることですがこんな利点があります。

処理の塊が視覚的に明確

例に挙げた UIButton の生成の場合、インスタンス生成後に画像だったりタイトルだったりと結構な量のコードを書いたりします。普通に書いた場合これがフラットに並んでしまうのでどこまでが button の設定なのかが少し見づらい感じになってしまいます。(実際にはプロパティーで宣言してゲッター内で生成処理を書くのでスコープとかも明確な事が多いとは思いますが)今回知った手法だと括弧で囲われてインデントされるので視覚的にわかりやすいです。

変数のスコープが切れることで変数名衝突回避

今回の例だとあまり困ることはないかもしれませんが、frame 等を複数の弄るようなコードを書く場合、スコープが別れていない場合には一時変数の名称が冗長になったり使い回しをして変な挙動になったりしていろいろと辛い感じになります。今回知った手法だとスコープが別れることで CGRect frame とかっていう普通の名前を連発しても問題ありません。

エラーが出ることで代入忘れを防止

以前書いたエントリと同様の効果ですが、最後の行に戻り値を書いてないとエラーがでるので特に Frame を弄ったあとに Frame を代入しわすれて期待の動きにならないっていう凡ミスが防げます。

まとめ

正直、厳格な仕様を理解しきれていないので本当に使っても大丈夫なのか?挙動に罠がないのか?という部分はわかりませんが、可読性と凡ミス防止には有効だと思いますし、以前書いたエントリのような下準備も不要で気軽に使えそうなのがいいですね。

CGRectDivide のすすめ

前回も座標系だったんですけど今回も座標系。

CGGeometry にはいろいろ地味に便利な関数が揃ってるんですけどあまり日の目を見ない気がするので CGRectDivide について書こうかと思います。

使い方と得られる結果が理解しにくい

CGRectDivide は任意の Rect を指定した方向から切り取り2つの Rect に分割してくれる関数です。 定義は以下の通り。

void CGRectDivide (
   CGRect rect,
   CGRect *slice,
   CGRect *remainder,
   CGFloat amount,
   CGRectEdge edge
);

実行するとこんな感じ。

CGFloat amount = 44.0f;
CGRect baseRect = self.view.frame;
CGRect sliceRect     = CGRectNull;
CGRect remainderRect = CGRectNull;

CGRectDivide(baseRect, &sliceRect, &remainderRect, amount, CGRectMinYEdge);
NSLog(@"%s \nbaseRect:      %@\nsliceRect:     %@\nremainderRect: %@", __PRETTY_FUNCTION__, NSStringFromCGRect(baseRect), NSStringFromCGRect(sliceRect), NSStringFromCGRect(remainderRect));

この例の場合、

  • sliceRect には baseRect の画面上部(CGRectMinYEdge)から 44 pt(amount) の高さで切り取った rect
  • remainderRect には baseRect からさっきの sliceRect を取り除いた rect

が入ります。

まとめると

baseRect を CGrectEdge で指定した方向から amount 分を切り取った rect が sliceRect に。
そのあまりが remainderRect に入れられる。

ということになります。

これでも、私の物忘れのヒドさでは使い方を忘れてしまいそうなのでこんな関数群を定義して使っています。

UIKIT_STATIC_INLINE void CSCGRectVerticalDivideFromTop(CGRect baseRect, CGFloat amountFromTop, void(^resultBlock)(CGRect topRect, CGRect bottomRect)) {
    CGRect sliceRect     = CGRectNull; /// this would be top rectangle.
    CGRect remainderRect = CGRectNull; /// this would be bottom rectangle.
    
    /**
     baseRect を CGrectEdge で指定した方向から amount 分を切り取った rect が sliceRect に。
     そのあまりが remainderRect に入れられる。
     */
    
    CGRectDivide(baseRect, &sliceRect, &remainderRect, amountFromTop, CGRectMinYEdge);

    if (resultBlock) {
        resultBlock(sliceRect, remainderRect);
    }
}

UIKIT_STATIC_INLINE void CSCGRectVerticalDivideFromBottom(CGRect baseRect, CGFloat amountFromBottom, void(^resultBlock)(CGRect topRect, CGRect bottomRect)) {
    CGRect topRect = CGRectNull;
    CGRect bottomRect = CGRectNull;
    CGRectDivide(baseRect, &bottomRect, &topRect, amountFromBottom, CGRectMaxYEdge);
    //NSLog(@"%s \n%@\n%@\n%@", __PRETTY_FUNCTION__, NSStringFromCGRect(baseRect), NSStringFromCGRect(topRect), NSStringFromCGRect(bottomRect));
    if (resultBlock) {
        resultBlock(topRect, bottomRect);
    }
}

UIKIT_STATIC_INLINE void CSCGRectHorizontalDivideFromLeft(CGRect baseRect, CGFloat amountFromLeft, void(^resultBlock)(CGRect leftRect, CGRect rightRect)) {
    CGRect sliceRect     = CGRectNull; /// this would be left rectangle.
    CGRect remainderRect = CGRectNull; /// this would be right rectangle.
    
    /**
     baseRect を CGrectEdge で指定した方向から amount 分を切り取った rect が sliceRect に。
     そのあまりが remainderRect に入れられる。
     */
    
    CGRectDivide(baseRect, &sliceRect, &remainderRect, amountFromLeft, CGRectMinXEdge);

    if (resultBlock) {
        resultBlock(sliceRect, remainderRect);
    }
}

UIKIT_STATIC_INLINE void CSCGRectHorizontalDivideFromRight(CGRect baseRect, CGFloat amountFromRight, void(^resultBlock)(CGRect leftRect, CGRect rightRect)) {
    CGRect sliceRect     = CGRectNull; /// this would be left rectangle.
    CGRect remainderRect = CGRectNull; /// this would be right rectangle.
    
    /**
     baseRect を CGrectEdge で指定した方向から amount 分を切り取った rect が sliceRect に。
     そのあまりが remainderRect に入れられる。
     */
    
    CGRectDivide(baseRect, &sliceRect, &remainderRect, amountFromRight, CGRectMaxXEdge);
    if (resultBlock) {
        resultBlock(remainderRect, sliceRect);
    }
}

どっちから(上から/下から/左から/右から)amount 分を切り取るかを別関数として定義して Blocks で切り取った結果を返す感じです。これなら CGRectDivide の使い方を忘れても大丈夫。

使用例

UIViewController の view に UILabel を貼ったものです。

コード

CSCGRectVerticalDivideFromTop(self.view.bounds, 44, ^(CGRect topRect, CGRect bottomRect) {
    self.topLabel.frame = topRect;
    self.bottomLabel.frame = bottomRect;
});

結果

f:id:griffin-stewie:20130521165750p:plain


コード

CSCGRectHorizontalDivideFromRight(self.view.bounds, 100, ^(CGRect leftRect, CGRect rightRect) {
    self.leftLabel.frame = leftRect;
    self.rightLabel.frame = rightRect;
});

結果

f:id:griffin-stewie:20130521165753p:plain

使いどころ

結構いっぱいあると思います。

  • UITableViewCell なんかで左側に画像、右側には文字情報群を配置したい
  • ViewController の View の上端ないし下端にはバー的な UI を置いて残りの部分がメインコンテンツ領域という時の配置

オマケ

上下分割して、広い方をさらに
上下分割して、広い方をさらに
左右分割して、広い方をさらに
左右分割とかあえてやってみた。

コード

CSCGRectVerticalDivideFromTop(self.view.bounds, 44, ^(CGRect topRect, CGRect bottomRect) {
    self.topLabel.frame = topRect;
    CSCGRectVerticalDivideFromBottom(bottomRect, 44, ^(CGRect topRect, CGRect bottomRect) {
        self.bottomLabel.frame = bottomRect;
        CSCGRectHorizontalDivideFromLeft(topRect, 44, ^(CGRect leftRect, CGRect rightRect) {
            self.leftLabel.frame = leftRect;
            CSCGRectHorizontalDivideFromRight(rightRect, 44, ^(CGRect leftRect, CGRect rightRect) {
                self.rightLabel.frame = rightRect;
            });
        });
    });
});

結果

f:id:griffin-stewie:20130521165746p:plain

UIView の frame を Blocks を活用して変更する

昨日、なんで今までこれをやってなかったんだろう?と思ったこと。

frame 弄るのダルい

UIView の layout 時に

label.frame.origin.y = 5;

とかしたいところですけど出来ません。 この程度の事で

label.frame = CGRectMake(CGRectGetMinX(label.frame),
                         5,
                         CGRectGetWidth(label.frame),
                         CGRectGetHeight(label.frame));

とかはダルすぎるので、一回変数に受けておいてから

CGRect frame = label.frame;
frame.origin.y = 5;
label.frame = frame;

くらいが妥当なところでしょうか。

これだと以下のような問題によく陥ります。

  • 複数のViewのレイアウトを行う場合に一時変数名に困る
  • 変更した rect を View に突っ込むのをうっかり忘れる

Blocks 使えば良かったんだ

この問題に対して以下のようなカテゴリメソッドを定義して

- (void)cs_changeFrameWithBlock:(CGRect (^)(CGRect frame))block
{
    if (block) {
        self.frame = block(self.frame);
    }
}

こんな感じでやれば良かったわけです。

[label cs_changeFrameWithBlock:^CGRect (CGRect frame) {
    frame.origin.y = 5;
    return frame;
}];

こうすることで

  • Block のスコープに縛られるので他の一時変数と被ることはない
  • 外の情報には問題なくアクセスできる
  • CGRect を return し忘れるとビルドに失敗するのですぐ気づく
  • ある View に対する frame の変更のコードブロック(塊)がわかりやすくて見やすくなる

まとめ

すでにそんなことやってるよとかもっといい方法を実践してるよっていう感じでしょうが、こんなくだらない小ネタでも気付きになるかと思って書きました。以前にも Blocks を使ったちょっとした便利な使い方を書きました。これからも何か気付きがあれば書いてみようと思います。

Heroku に Sinatra でゴニョゴニョした時の備忘録

GW に親父に頼まれてこんなのを作ったときの備忘録

  • 複数のサイトからデータをスクレイピングして tsv ファイルに書き出し
  • tsv ファイルをまとめて zip
  • これらを Web アプリとして提供

本当は親父の PC ローカルで完結させたかったんだけど、Windows だし、環境構築がダルいしってことで、RubySinatra で Heroku に設置するかんじになりました。

僕のバックグラウンドとしてはこの程度

  • iOS & Objective-C しか知らない
  • Ruby は細々と書き捨てなスクリプトは書いたことがある
  • JavaScript は大昔に Sleipnir のスクリプトを書いたことがある

ということで

  • Ruby でそれなりのコード量を書くのははじめて
  • Web アプリ的なのははじめて
  • Heroku を使うのも Hello World 以外で使うのははじめて

という感じだったので大分屈折した感じで間違ったことを書いているとは思います。

スクレイピング

基本的に Nokogiri でやりました。このライブラリは割とよく使っているので慣れてはいました。DOM の指定はなんとなく css セレクタを使ってます。Chrome とかの Web Inspector とかだと Copy XPath とかあるからそっちの方が楽だったかもとか思ったりもしますがまあ勉強になるのでいいかなぁと。

ハマったのはあるサイトの pre タグ内に "<1234>" みたいな文字があったのですが Nokogiri だとこの文字列が欠落してしまうことがありました。Hpricot を使ってみたらどうなるかなぁと思ったら、inner_text メソッドを呼び出すと Hpricot 内部で落ちてしまって万事休す。かと思いましたが、以下のようにして open して取ってくるときにバイナリで取ってくるようにしたらなんとか期待通りの動作になりました。

Hpricot(open(url, "r:binary").read.toutf8)

Sinatra

Background Job

構成としては以下のようなシンプルな画面です。

  • スクレイピング開始用のボタン
  • 生成できた zip ファイルをダウンロードするボタン

問題だったのはスクレイピング開始周りです。当然ながらスクレイピングは時間がかかります。同期的に処理をしてしまうと timeout してしまってどうにもなりません。iOS 的なコードでいうところのこんな感じの処理を書きたくなりました。

- (id)startScraping
{
    self.cleanUp;

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(queue, ^(void) {
        [self scrapeWithURL:url];
    });
    
    return response;
}

スクレイピング自体はバックグラウンドに回してしまってとりあえずはレスポンスを返しておくイメージです。調べてみると Heroku で Sinatra を動かす場合 EventMachine の中で動いているらしいので

EM::defer do
    doHeavyTask()
end

とすると Block 内部は別スレッドで実行できるようです。EM:defer は実行ブロックと完了ブロックを渡せるので iOS でやっていたようなノリでできます。

iOS で inline block を書くときはこんな感じで書けますが、

id(^completionBlock)(void) = ^(void) {
    return @"test";
};

Ruby だとこんな感じで書けるんですね。

callback = proc do
    ""
end

HTML

最初は erb を使っていましたが、

  • 構造は同じ
  • ただし、具体的な文字等はバラツキあり
  • 上記を複数回繰り返し出力したい

ということを実現する方法がよくわからなかったのですが、ググっていると haml を使って自分のやりたいことと似ていることをしているサンプルを見つけたのでやってみようと思ったところ実際にできたので途中から haml に変更しました。

CSS

よく分からないので Twitter Bootstrap を使って適当にサンプルから HTML をコピペしました。

まとめ

初体験でしたが、GW のアソビとしてはなかなか楽しかったです。iOS で学んだものが多少なりとも活きている気がしたのがうれしかったです。課題としてはスクレイピングが終わったらページの書き換えを出来るようにしたいです。誰かやり方教えてください。

複数の非同期処理を前の処理結果を受けとりつつ数珠つなぎに直列に書きたかった時に参考にしたコード

以前

@xcatsan さんのこちらの記事をさっきみてて思い出したので備忘録として。

以前、非同期で実行される処理の塊を直列に順序を決めて実行したいというシチュエーションがありました。そのときにぱっと浮かんだのは昨日の Cocoa 勉強会関西で@yashigani が発表した内容 にも登場した NSOperationNSOperationQueue でした。「非同期で実行される処理の塊を直列に順序を決めて実行」だけならそれでも良かったんですが、その処理の塊の結果を次の処理の塊に受け渡す必要がありました。さらにその処理結果によって次以降に実行する処理の塊も振り分けるとか…。基本的に非同期処理のコールバックは Blocks を使うことは僕の中できまっていたので Blocks の入れ子とか地獄なのは分かっていたのでどうにかしたかった。

やりたかったこと要求されてたこと

  • UI から1つのタスク(大)が依頼される
  • そのタスク(大)は一個のメソッドとして実装され、 completionBlock でその結果を UI に返される
  • タスク(大)は実は複数のタスク(中)を数珠つなぎにしたもの
  • たまにタスク(大)はタスク(中)の実行状況に合わせてタスク(中)を追加したりする
  • 実はタスク(中)もタスク(小)の集合だったりする

参考にしたライブラリ

前振りが無駄に長かったですがそのときに参考にしたライブラリを列挙します。

STDeferred

今回の要求を聞いて最初に思い浮かんだのがこのライブラリ。処理結果を次の結果に受け渡しています。 1つのメソッドに Blocks を数珠つなぎになる感じだったのでそのまま使うことはなかったですが、受け渡しかたの参考にさせていただきました。

Sequencer

冒頭にあげた @xcatsan さんのこちらの記事でも紹介されています。参照先に書かれているとおりシンプルだったので参考になりました。STDeferred よりも書き方がシンプルに見えて好みでした。

AAMCommandKit

Serial 実行する Command とか参考にさせてもらいました。

結局こんな実装に

コードは諸事情で出せないのですが、うろ覚えの実装はこんな感じだったです。

  • NSOperationNSOperationQueue っぽいものを自作
  • オレオレ Operation クラスには前の処理結果を取り出すプロパティと自分の処理結果を入れるプロパティを設けた
  • オレオレ Queue は突っ込まれた Operation を実行し、キューイング時に、実行済みの Operation の処理結果を取り出して次の Operation の previousResult 的なプロパティに突っ込んでから次の Operation を実行
  • オレオレ Queue には Delegate が実装してあり Operation をキューイングする時に前の Operation と次の Operation を垣間見れてフックしたりもできる
  • Operation 内部では execute されたときに previousResult を見つつその内容によって実行内容を調整
    • 例えば前の処理がエラーだったら、そのエラーを自身の結果として詰め込むとか。つまり、1, 2, 3, 4 と処理が並べてあっても、もし 2 でこけてたらそのまま 2 がコケたよという結果をそのまま伝搬することで最終的に結果をコールバックされた呼び出し元(タスク(大)の呼び出し元)では誰がコケたのかわかる。
  • Queue には全 Operation の実行が完了したことを伝える Delegate を実装

懺悔

参考にしたライブラリを複雑そうだ思ったりもした割に、自分の実装の方がはるかに複雑です。ごめんなさい。

継承とかも入ってたので複雑度マックスです。ごめんなさい。

全体像の把握がツライです。ごめんなさい。

でも、そのプロジェクト内では、新規タスクの追加や仕様変更にはそこそこ強かったのでこれはこれでよかったかなと思ってます。

次回似たような要求があったときにはもっと整理した形で実装したいと思っています。

最近よくやってる Blocks の使い方

Blocks 怖いような、便利なような。そんな物ですがみなさんはどんな感じに使っていますか?
僕は最近こんな感じのメソッドを実装して使ってます。

UINavigationController *nvc = [DetailViewController navigationBasedViewControllerWithConfigurationBlock:^(DetailViewController *viewController) {
        NSDate *object = _objects[indexPath.row];
        viewController.detailItem = object;
}];
[self.navigationController presentViewController:nvc animated:YES completion:NULL];

特に凄く便利かというとそうでもないと思うんですけど、典型的なモーダルな ViewController を表示させるコードってこんな感じだと思います。

DetailViewController *vc = [[DetailViewController alloc] init];
NSDate *object = _objects[indexPath.row];
vc.detailItem = object;
UINavigationController *nvc = [[UINavigationController alloc] initWithRootViewController:vc];
[self.navigationController presentViewController:nvc animated:YES completion:NULL];

要するにこんなことをしてるんですよね。

  • 表示させたい ViewController クラスを生成
  • 表示させたい ViewController クラスに何か値をセット
  • NavigationController を生成
  • 表示

いろいろとタイプすることが多いのをちょっとだけ省略したくてこうしてます。

実装はこんな感じです

+ (UINavigationController *)navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock
{
    UIViewController *vc = [[[self class] alloc] init];
    if (configurationBlock) {
        configurationBlock(vc);
    }
    return [[UINavigationController alloc] initWithRootViewController:vc];
}

引数で受けた Block は同期的な感じで使うのでコピーしてないので循環参照を気にする必要もないですね。 この例では UIViewController の生成を init にしちゃってますがそのあたりは実際の実装との相談でバリエーションを持たせてもいいかもしれないです。 Block に渡す引数の型は id 型にしておけば、呼び出し側で任意のクラス型に書き換えられるので、ブロック内でキャストする必要もないです。 カテゴリメソッドとして実装するとある程度汎用的に使えます。

思いつきで進化版

もっと発展させて、さっき思いつきで実装したカテゴリメソッドはこんな感じで使います。

__weak typeof(self) weakSelf = self;
UINavigationController *nvc = [DetailViewController cs_navigationBasedViewControllerWithConfigurationBlock:^(DetailViewController *viewController) {
    NSDate *object = _objects[indexPath.row];
    viewController.detailItem = object;
} wantsDismiss:^(DetailViewController *viewController) {
    [weakSelf.navigationController dismissViewControllerAnimated:YES completion:^{
        // do something
        NSLog(@"%s dismissViewControllerAnimated: %@", __PRETTY_FUNCTION__, weakSelf);
    }];
}];

昔、師匠に教えてもらったんですけど「モーダルを閉じるのはモーダルを出した側がやる方がいい」と。Apple のサンプルでも Delegate 経由でモーダルを出した側からモーダルを閉じるようにしているのがあります。あれって実装するのが面倒くさくてついついモーダル側で自身を閉じてしまいがちでした。このカテゴリメソッドではモーダル側の例えば cancel ボタンなりを押されたときにインスタンス生成時に渡された Block を呼ぶことで呼び出し側に閉じるか否かを委譲させています。さすがに今回は引数として受けた wantsDismiss Blcok は Copy して保持しているので循環参照対策として weak な self を使っています。実装はこんな感じです。

#import "UIViewController+CSAdditions.h"
#import <objc/runtime.h>

static char kUIViewControllerCSAdditionsKey;

@implementation UIViewController (CSAdditions)
+ (UINavigationController *)cs_navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock
{
    UIViewController *vc = [[[self class] alloc] init];
    if (configurationBlock) {
        configurationBlock(vc);
    }
    return [[UINavigationController alloc] initWithRootViewController:vc];
}

+ (UINavigationController *)cs_navigationBasedViewControllerWithConfigurationBlock:(void(^)(id viewController))configurationBlock
                                                                       wantsDismiss:(void(^)(id viewController))callbackBlock
{
    UIViewController *vc = [[[self class] alloc] init];
    if (configurationBlock) {
        configurationBlock(vc);
    }
    
    if (callbackBlock) {
        [vc cs_setWantsDismisCallbackBlock:callbackBlock];
    }
    
    return [[UINavigationController alloc] initWithRootViewController:vc];
}

- (void)cs_setWantsDismisCallbackBlock:(void(^)(id viewController))callbackBlock
{
    objc_setAssociatedObject(self,
                             &kUIViewControllerCSAdditionsKey,
                             callbackBlock,
                             OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void(^)(id viewController))cs_wantsDismisCallbackBlock;
{
    return (void(^)(id viewController))objc_getAssociatedObject(self, &kUIViewControllerCSAdditionsKey);
}
// DetailViewController 側の使用例
// DetailViewController の leftBarButtonItem に設置した Cancel ボタンを押したときに呼ばれるメソッド
- (void)wantsDismis:(id)sender
{
    /// 登録されている Block を呼び出して引数として自身をわたす
    [self cs_wantsDismisCallbackBlock](self);
}

associatedObject を使って wantsDismisCallbackBlock を保持するようにしています。保持した wantsDismisCallbackBlock の解放をどうしようかと思いましたが、明示的に解放しなくても自分自身が解放されるときにいっしょに解放してくれるようだったので特に何もしていません。

Blocks ってやっぱり便利

とはいえ、Blocks は万能ではないので traditional な delegate パターンや target-action パターンなんかもうまく活用していきたいですね。

簡単なバージョン番号の比較方法

最近、頑張って比較してるのにバグっている見たコードをみました。簡単な比較方法を知らない人もいるのかもと思って書いてみます。

たまに求められるバージョン番号比較

何らかの理由でバージョン番号の比較をしたいときってありますよね?○○バージョン以上なら△△するとか。求められる内容によって

  • Class オブジェクトが生成できるかどうか?
  • respondsToSelector: で期待しているセレクターが呼べるか?

とか方法はいろいろありますが、今回は文字列での比較の話です。

文字列ベースで比較

文字列ベースでのバージョン番号の比較ってどうしてますか?

integerValue とか floatValue とかで数値化して比較してますか?最近見たコードでもそのような比較をしてました。でも、バージョン番号って 「5.1」とかなら単純に数字にしてしまって比較できますが、「5.1.1」とかだと少数でもないですよね?バラして数値化して比較することもできますがちょっと面倒ですし、バグりそうです。実際に僕が見たコードはバグってました。

NSNumericSearch

こんなときは NSString の

- (NSComparisonResult)compare:(NSString *)aString options:(NSStringCompareOptions)mask

を使うと簡単です。

compare:options: の option 引数で NSNumericSearch を指定すると数字を含んだ文字列を数字として評価して結果を返してくれます。これは Mac の Finder でのファイル名ソートにも使われています。 "file_1.txt", "file_5.txt", "file_10.txt" とかってファイルをソートするとちゃんと 1 の 後には 5, 10 と続きますよね?OSとかによっては 1 の後が 10, 5 ってソートされて残念な気分になったことがあります。

具体例 1

NSComparisonResult result = [@"4.1.1" compare:@"3" options:NSNumericSearch];  
// result is "NSOrderedDescending"

上記の場合は Descending なので

"4.1.1" > "3"

ということです。

具体例 2

NSComparisonResult result = [@"5.1.1" compare:@"6.0" options:NSNumericSearch];  
// result is "NSOrderedAscending"

つまり

"5.1.1" < "6.0"

具体例 3

NSComparisonResult result = [@"6.1" compare:@"6.1" options:NSNumericSearch];  
// result is "NSOrderedSame"

つまり

"6.1" = "6.1"

ですね。

具体例 4

こんな変な比較も人間的な感覚にマッチした比較結果を返してくれます。

NSComparisonResult result = [@"4.0.0.1.0.0" compare:@"4.0.0.0" options:NSNumericSearch];
// result is "NSOrderedDescending"

具体例 5

僕はこんな感じのコードを UIDevice のカテゴリメソッドで生やしています。

+ (BOOL)cs_isOSVersionGreaterThanOr:(NSString *)version
{
    NSString *currentVersion = [[self currentDevice] systemVersion];
    NSComparisonResult result = [version compare:currentVersion options:NSNumericSearch];
    return (result == NSOrderedAscending || result == NSOrderedSame) ? YES : NO;
}

注意点

  • 引数に渡す文字列が nil の場合には戻り値が不定(Reference より)
  • レシーバ側が nil の場合は 0 が返るので結果が NSOrderdSame になる
  • [@"6" compare:@"6.0" option:NSNumericSearch] の結果が NSOrderedAscending を返してしまう(2013/02/25 追記)

注意点が増えました 3番目の問題は @norio_nomura さんに指摘していただきました。これは地味にうざい挙動です。やらかしてしまいそうです。

そつなく

compare:options: は Foundation.framework で結構古くから実装されていますし Mac OS でも実績があるはずの枯れたものですのでバグがある可能性はあまり考えられません。注意点に上げたポイントさえ気をつければヘタに文字列を分割して数値化して比較するコードを手で書くよりも簡単で無難ですのでどんどん使いましょう。