Displaying posts tagged with

“Objective-C”

UINavigationControllerによる「戻る」「進む」を検出する

UIViewControllerのviewWillAppear:、viewDidDisappear:等のメソッドはviewの表示状態が変化した時に呼ばれるが、なぜ変化したのかという理由までは分からない。つまり以下のようなイベントが簡単には区別できない:

  • UINavigationControllerの中で画面遷移が起こった(戻った・進んだ)
  • modalダイアログが開かれた・閉じられた
  • UITabBarControllerでタブが切り替えられた

今作っているアプリでは特にUINavigationControllerの中で「進んだ」「戻った」といったイベントを検出したかったので、それらをサポートするための抽象クラスを作ってみた(UIViewControllerのサブクラス)。

NavigationSupportController.h
NavigationSupportController.m
(iOS 4.3と5.0で動作確認)

このNavigationSupportControllerのサブクラスでは、viewWillAppear:、viewDidDisappear:等の代わりに次のメソッドをオーバーライドする。

  • viewWillAppear:(BOOL)animated direction:(NavigationDirection)direction
  • viewDidAppear:(BOOL)animated direction:(NavigationDirection)direction
  • viewWillDisappear:(BOOL)animated direction:(NavigationDirection)direction
  • viewDidDisappear:(BOOL)animated direction:(NavigationDirection)direction

NavigationDirectionは列挙型で、次の3種類の値をとる。

NavigationBackUINavigationControllerによる「戻る(=pop)」で画面遷移が起こった
NavigationForwardUINavigationControllerによる「進む(=push)」で画面遷移が起こった
NavigationNoneUINavigationControllerによらない表示状態の変化が起こった

こんな感じで判定する。

- (void)viewWillAppear:(BOOL)animated direction:(NavigationDirection)direction
{
    if (direction == NavigationBack) {
        //「戻る(= pop)」の場合
    } else if (direction == NavigationForward) {
        //「進む( = push)」の場合
    } else if (direction == NavigationNone) {
        //NavigationControllerによらない画面遷移の場合
    }
 
    //共通の処理はここに書く
    //つまり viewWillAppear:(BOOL)animated の方はオーバーライドすべきではない
}

検出ロジックの都合上、コントローラのインスタンスはpushするときに作成され、popされるときに破棄されることを前提としている。普通はこれで問題ないはず。

もっと確実・簡単な方法があれば教えてください。

iPhoneの機種を判別するUIDevice Extension

iPhone 5の発表間近といわれる今日この頃。

アプリ内でiPhone(iOSデバイス)の機種を判別するには sysctlbyname というC言語の関数を使うのだが、結果は “iPhone3,1″ のような文字列で返ってくるため、そのままでは扱いづらい。これをわかりやすい形に変換してくれるUIDevice Extensionというオープンソースのライブラリが存在する。

erica / uidevice-extension – GitHub BSDライセンス
(注: forked from ars/uidevice-extension とあるが、arsとericaはともにErica Sadunさんのアカウントなので、これが本家リポジトリと考えて良いようだ)

UIDevice Extensionには多くの機能があるが、機種判別を行うだけなら UIDevice-Hardware.{h,m} のみ自分のプロジェクトにコピーすれば良い。いずれもUIDeviceクラスに対するカテゴリとして構成されているので、使用したい箇所で .h をインクルードすれば、UIDeviceに専用のメソッドが追加される。

機種名を文字列として取得するには platformString メソッドを使う。このメソッドが適しているのはユーザがどの機種を使っているか集計したい場合等だろう。なお2011年9月25日現在、未発表のiPhone 5まで対応している(この情報が確実に正しいとまでは言えないはずだが)。

//@"iPhone 3GS" や @"iPhone 4" といった文字列が返る
NSString *iosDevice = [[UIDevice currentDevice] platformString];
 
//おまけ: iOSのバージョンとアプリのバージョンを取得する
NSString *osVersion = [[UIDevice currentDevice] systemVersion];
NSString *appVersion = [[[NSBundle mainBundle] infoDictionary]
                                        objectForKey:(NSString*)kCFBundleVersionKey];

機種に応じてロジックを切り替える必要がある場合には platformType メソッドが適している。戻り値はヘッダファイル内で定義された列挙型 UIDevicePlatform である。

if([[UIDevice currentDevice] platformType] == UIDevice4iPhone) {
    //iPhone4 の場合の処理
}

ただ大雑把に機種を振り分ける場合にはコードが煩雑になりがちなので、jorgenptさんのforkで定義されているような包括的なマクロを利用するのが良いかもしれない。いずれにせよこの種の判定でロジックを切り替える場合、前方互換性に注意する必要がある。

UITableViewのスクロール位置をView再読み込み時に復帰する方法

メモリ警告によってViewが破棄された場合、UITableViewのスクロール位置はリセットされてしまう。再度Viewが読み込まれたときに元の位置に戻すには、破棄される直前のスクロール位置を覚えておく必要がある。だいたい次のようなコードになる:

ScrollTestViewController.h

#import <UIKit/UIKit.h>
 
/* UITableViewControllerではなくUIViewControllerを使っている理由は後述 */
@interface ScrollTestViewController :
UIViewController<UITableViewDataSource, UITableViewDelegate> {
 
    /* UITableViewはIBOutletでretainする想定 */
    UITableView *_tableView;
 
    /* Viewが破棄される直前のスクロール位置 */
    CGPoint lastScrollOffset;
}
@property (nonatomic, retain) IBOutlet UITableView *tableView;
 
@end

ScrollTestViewController.m

@implementation ScrollTestViewController
 
@synthesize tableView = _tableView;
 
- (void)viewDidLoad {
    [super viewDidLoad];
 
    /*
     reloadDataでデータを読み込んだ後に
     記録されているスクロール位置を復帰
     初期化直後は{0,0}で初期化されているので影響はない
    */
    [_tableView reloadData];
    [_tableView setContentOffset:lastScrollOffset];
}
 
- (void)viewDidUnload {
    [super viewDidUnload];
 
    /*
     スクロール位置を記録した後でUITableViewを破棄する
     viewDidUnloadが呼ばれた時点でメインのViewは既に破棄されているが、
     Controllerがretainしているサブビューはまだ生きているというのがポイント
    */
    lastScrollOffset = [_tableView contentOffset];
    self.tableView = nil;
}
 
- (void) dealloc {
    self.tableView = nil;
    [super dealloc];
}
 
/*
...
その他、UIViewControllerでUITableViewを使うためには
viewWillAppearで選択を解除する処理などが必要だが
本題には関係ないので省略
...
*/
 
@end

ここでUITableViewControllerを使ってしまうと、UITableViewが破棄されるタイミングや再読み込みされるタイミングを制御できなくなるので、記憶するのも元に戻すのも難しくなる。例えばUITableViewのreloadDataをオーバーライドする必要があったりするらしい。

参考: iPhone 画面切り替え時、あらかじめテーブルビューを目的の位置にスクロールさせとく技

IBOutletで接続した変数はreleaseする必要があるのか

iOSとMac OS Xとで結論が異なる。OS X開発経験者が書いた書籍やWebサイトはこの違いを考慮していない場合があるので注意が必要。

……

IBOutletの宣言はインスタンス変数にもプロパティにも書くことができる。

@interface IBOutletTestAppDelegate : NSObject <UIApplicationDelegate> {
    UILabel *_retainLabel;
    UILabel *_assignLabel;
    IBOutlet UILabel *_variableLabel;
}
 
@property (nonatomic, retain) IBOutlet UILabel *retainLabel;
@property (nonatomic, assign) IBOutlet UILabel *assignLabel;
@end

iOSの場合、IBOutletをどちらに書いても、アウトレットの接続は常にNSObject(NSKeyValueCoding)の setValue:forKey: メソッドを使って行われる。このメソッドは(1)まず対応するプロパティのsetterを探して、(2)見つからない場合はインスタンス変数に代入してretainカウントを1増やす。よってassignプロパティを使うのでない限り、アウトレットで接続した変数は常にreleaseしなければならない

この動作は setValue:forKey: をオーバーライドしてみれば確認できる。

- (void)setValue:(id)value forKey:(NSString *)key
{
    //retainのプロパティとインスタンス変数の場合は
    //retainCountが1増える
    NSLog(@"[before setValue] %@ %d", key, [value retainCount]);
    [super setValue:value forKey:key];
    NSLog(@"[aflter setValue] %@ %d", key, [value retainCount]);
}

ただしAppleが提供するGuideでは常にプロパティを使うことを推奨している(Memory Management Programming Guideの『Memory Management of Nib Objects』など)。プロパティを使うことでretain/assignのポリシーが明確にできるし、retain/assignどちらの場合もviewを破棄する際にはnilを代入するだけで良い。特にretainの場合はreleaseとnil代入の操作を一度にできるので便利である。

……

Nibファイルによるオブジェクトの生成と、生成されたオブジェクトのretain状況についてはResource Programming Guideの『The Nib Object Life Cycle』にまとめられている。これを読むとOS Xの方が(GCを使わない場合)状況が複雑だということがわかる。

簡単にまとめると

iOSの場合

  • 生成されたオブジェクトは全てautorelease済みである
  • アウトレットの接続は setValue:forKey: メソッドで行われる

OS X(GCなし)の場合

  • 生成されたオブジェクトは、トップレベルのオブジェクトを除いてautorelease済みである。
  • アウトレットの接続は、(1)まずset{OutletName}:というメソッドを探して、(2)見つからない場合はインスタンス変数にretainカウントを増やさずに代入される

OS X(GCなし)の場合、トップレベルのオブジェクとそれ以外のオブジェクトとで扱いが異なるので、一貫したポリシーを立てるのが難しくなっている。もしretainのプロパティを使った場合、トップレベルのオブジェクトは2回releaseしなければならない。もしインスタンス変数に直接代入した場合、トップレベルのオブジェクトに限りreleaseしなければならない。

External Objectを使ってIBOutletからNibファイル外のオブジェクトに接続する

External Objectを使うことで、Nibファイル外のオブジェクトに対してIBOutletで接続できる。ちなみにこの機能はMac OS Xでは使用できず、iOSでのみ使える。

……

たとえばあるカスタムビューからモデルのインスタンスを参照したいとする。このビューでは常にモデルの状態を反映した表示を行いたいので、インスタンス変数にモデルへの参照を保持しておきたい。

ビューの生成・初期化をプログラム中で行うなら、initWithModel:のような初期化子を用意しておけば済む。しかしビューをNibファイルから生成する場合は初期化時にモデルを参照することができない(モデルは普通Nibファイルで生成しないので)。仕方がないのでモデルのインスタンスだけはUIViewControllerのviewDidLoadでビューに代入して、専用の初期化メソッドを呼び出すなどするしかない。これは非常に面倒だし忘れやすい。

- (void)viewDidLoad
{
    [super viewDidLoad];
 
    //プロパティに代入してから表示を更新。面倒くさい
    _customView.model = _model;
    [_customView update];
 }

そこでExternal Objectの出番である。このオブジェクトはNibファイル外の既成のオブジェクトに対するPlaceholderとして機能する。ちょうど自由な名前をつけられる「File’s Owner」だと考えれば良い。

使い方は次のようになる。まずカスタムビューの.hファイルで、モデルに対するOutletを設定しておく。

@interface CustomView : UITableViewCell
{
    Model *_model;
}
 
@property (nonatomic, retain) IBOutlet Model *model;

モデルを使った初期化処理は、その他の初期化処理と同じくawakeFromNibの中に書くことができる。

- (void)awakeFromNib
{
    [super awakeFromNib];
 
    //モデルのデータを使って表示を初期化する
    _textField.text = _model.name;
}

続いてxibファイルを開いてExternal Objectの設定を行う。Libraryの中から「External Object」を選んでObjectsの欄にドラッグアンドドロップすると、Placeholdersの欄にオブジェクトが追加される。

追加されたオブジェクトを選択してIdentity inspectorを開き、「Custom Class」の中でモデルクラスのクラス名を指定する。

次にAttributes inspectorを開き、Identityの欄にxibファイル中で一意な識別子を設定する。この識別子が実際のオブジェクトを割り当てる際のキーになる。

あとは通常のOutlet接続と同じ操作で、カスタムビューのOutletプロパティからPlaceholders内のモデルに対して接続を行っておく。

これでxibファイルの準備ができたので、あとはNib読み込みの際にExternal Objectとして参照されるオブジェクトの実体を指定する。これにはNSBundleクラスの loadNibNamed:owner:options: メソッドの UINibExternalObjects オプションを使う。

Nibファイルの読み込みはUIViewControllerのloadViewメソッで行うのが良いだろう。以下のサンプルコードではコントローラがモデルのインスタンスを変数 _model に保持しており、xibファイル内でExternal Objectには Model というidentityを割り当てたと想定している。YourNibFileNameは読み込むNibの名称。

- (void)loadView
{
    NSArray *topLevelObjs = nil;
    NSDictionary *proxies = [NSDictionary dictionaryWithObject:_model forKey:@"Model"];
    NSDictionary *options = [NSDictionary dictionaryWithObject:proxies forKey:UINibExternalObjects];
    topLevelObjs = [[NSBundle mainBundle] loadNibNamed:@"YourNibFileName" owner:self options:options];
 
    if ([topLevelObjs count] == 0) {
        NSLog(@"Warning! Could not load nib file.\n");
        return;
    }
}

これによって特別な初期化操作をしなくても、Nibファイル内のカスタムビューとコントローラとで一つのモデルのインスタンスを共有することができる。

参考: Resource Programming Guide

ImageStore(改造版)を使って画像を非同期ダウンロードする

ImageStoreはiOSで画像を非同期ダウンロードするためのObjective-Cライブラリです。Satoshi Nakagawaさんがオープンソースのライブラリとして公開されています(New BSDライセンス)。手軽に使えて非常に便利なライブラリなのですが、そのままアプリに組み込むには少々難があります。

  1. ダウンロードをキャンセルする手段がない
  2. ImageStoreのインスタンスを破棄する以外に、キャッシュをクリアする方法がない
  3. 正常に画像がダウンロードできたかどうか判定する術がない

1がないので、ダウンロード中に画面移動が発生したりすると容易にクラッシュします。2がないので、随時キャッシュを消して再ダウンロードしたり、メモリ不足時にキャッシュを削除したりといったことができません。3のため、サーバが404を返した場合等にはリクエストが無限ループしてしまう危険があります(ImageStoreの中に含まれるサンプルアプリではそういう仕様になってしまっています)。また画像が取得できなかった時に代替画像を表示するといった処理も実装できません。

そこでオリジナル版からforkして、これらの不具合を解消した改造版を作りました。

https://github.com/tkyk/imagestore

(4月6日追記: 私の改造版とは使い方が異なりますが、本家ImageStoreでも同等の修正が行われました)

ImageStoreクラスには4つのメソッドが増えました。どんなメソッドかは名前から想像がつくと思います。

- (void)cancelAllDownloads;
- (void)cancelDownloadFromUrl:(NSString*)url;
- (void)clearAllCaches;
- (void)clearCacheForUrl:(NSString*)url;

ImageStoreDelegateにも新たにメソッドが宣言されています。

- (void)imageStoreDidFailNewImage:(ImageStore *)sender
                              url:(NSString *)url
                    fallbackImage:(UIImage**)anImage;

使い方

UITableViewの各Cellに画像を表示する処理を例に、使い方を説明していきます。現実のiPhoneアプリの開発で良くあるパターンだと思います。

まず tableView:cellForRowAtIndexPath: で UITableViewCell の表示を更新するときにgetImageを呼び出します。画像が既にキャッシュされていればその場でUIImageのインスタンスが返却され、キャッシュがなければnilが返されて非同期の画像ダウンロードが開始されます。

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell;
    //...略...
 
    //このindexPathに対応する画像のURL
    NSString *imageURL = [self getImageURLAtIndexPath: indexPath];
 
    //getImageにURLを渡す
    cell.imageView.image = [imageStore getImage: imageURL];
 
    return cell;
}

画像が正常に取得できるとImageStoreDelegateの imageStoreDidGetNewImage:url: が呼ばれるので、reloadDataを呼び出してセルの表示を更新します。これによって再度getImageが呼ばれ、キャッシュから画像が取り出されます。

- (void)imageStoreDidGetNewImage:(ImageStore *)sender url:(NSString *)url
{
    [self.tableView reloadData];
}

ここまではオリジナル版のImageStoreと同じ流れです。

しかしオリジナル版では画像の取得に失敗した場合にもこのメソッドが呼ばれてしまうため、リクエストが(成功するまで)無限にループしてしまいます。そこで私の改造版では画像の取得に失敗した場合、具体的には

  • ステータスコードが200番台では無い
  • MIMETypeがimage/*ではない
  • UIImageのinitWithDataがnilを返した

こういった場合、 代わりに imageStoreDidFailNewImage:url:fallbackImage: が呼び出されます。

// 代替となる画像(fallbackImage)を事前に読み込んでおく
- (id)initWithCoder:(NSCoder *)aDecoder
{
    //..略
    fallbackImage = [[UIImage imageNamed:@"no_image"] retain];
    return self;
}
 
// 読み込み失敗時にはこのメソッドが呼ばれるので、
// fallbackImageを代替画像として使用
- (void)imageStoreDidFailNewImage:(ImageStore *)sender
                              url:(NSString *)url
                    fallbackImage:(UIImage **)anImage
{
    *anImage = fallbackImage;
}

第一引数はImageStoreのインスタンス、第二引数は失敗したURLで、第三引数は代替画像(UIImageのインスタンス)に対するポインタです。このポインタに非ゼロの値をセットしておくと、その画像が代わりにキャッシュされた上で imageStoreDidGetNewImage:url: が呼び出されます。つまり「正常に取得できなかった」という結果をキャッシュすることができる、ということになります。

キャッシュのクリア

一般に、didReceiveMemoryWarningでは全画像キャッシュをクリアしておくべきでしょう。

- (void)didReceiveMemoryWarning
{
    [imageStore clearAllCaches];
    [super didReceiveMemoryWarning];
}

その他に、ダウンロードに失敗した画像のURLを記録しておいて、「再読込」操作時にキャッシュをクリアして再リクエストする、といった用途も考えられます。

参考: ImageStoreのコードリーディング – haoyayoi Dev Style

ダウンロードのキャンセル

多くの場合、viewWillDisappear:のタイミングで全ての画像のダウンロードをキャンセルするのが良いだろうと思います。

- (void)viewWillDisappear:(BOOL)animated
{
    [imageStore cancelAllDownloads];
    [super viewWillDisappear:animated];
}

ただしこのままだとダウンロード中に別の画面に移動した場合、ダウンロードが完了していなかった画像がそのままになってしまうので、戻ってきたときに自動でダウンロードが再開されるよう、viewWillAppear:でセルの表示を更新する必要があります。

- (void)viewWillAppear:(BOOL)animated
{
    [self.tableView reloadData];
    //またはvisibleCellsを辿って表示を更新するなど
    //for(UITableViewCell *cell in [self.tableView visibleCells])
    [super viewWillAppear:animated];
}

iOS開発におけるパターンによるオートマティズム レビュー

新しい知識を与えてくれる本ではない。ページ数も200ページ弱しかない。ある程度のiOS開発経験者なら、初めて知る内容はほとんど無いだろう。しかし数多の知識よりも遥かに価値ある知見を提供してくれる本だ。

以前も書いたが、iOS初学者が入門書を終えた後で「知識」を増やすには、下手に本を読んだりWebを漁ったりするより、Appleが提供している公式のプログラミングガイドを読むのが一番いい。フレームワークの各コンポーネントにどんな機能があり、どんな使い方があるか、他のどんな文書より網羅的に知ることができる。

しかし100通りのやり方を知っていても、実際にアプリケーションを作る時には、その中からたった1通りのやり方を選ばなければならない。その時その状況にあったベストなやり方を選ぶのはとても難しい。単に知識を増やすだけでは不十分で、フレームワークの設計思想や、作ろうとしている機能の性質についての深い理解、そして何より多くの経験・試行錯誤に基づく知見が必要だ。

この本が提供しようとしているのがまさにそれである。「いろいろ他の方法も考えられるが、この方法がベストである。なぜなら…」と本書は説く。プログラマの三大美徳としての”傲慢さ”に溢れた本である。著者を深く尊敬せずにはいられない。

というわけで、私もその傲慢さに倣って、いくつかの内容について強化/改善案を提案したい。

メモリ管理のパターン[4]について

この本では「インスタンス変数はretainし、releaseした後は常にnil代入する」という原則が貫かれている。とても合理的で分かりやすく、早速真似させてもらったのだが、deallocメソッドの中で同じ形の式が繰り返されるのがちょっと見苦しい。

- (void)dealloc
{
    [_identifier release], _identifier = nil;
    [_title release], _title = nil;
    [_link release], _link = nil;
    [_itemDescription release], _itemDescription = nil;
    [_pubDate release], _pubDate = nil;
 
    [super dealloc];
}

Xcodeの補完機能は優秀なので、こういった繰り返しを書くこと自体はさほど苦ではないのだが、個人的に見た目が(生理的に)我慢できないので、次のようなマクロを書いた。

#define DESTROY(x) [x release], x = nil
 
- (void)dealloc
{
    DESTROY(_identifier);
    DESTROY(_title);
    DESTROY(_link);
    DESTROY(_itemDescription);
    DESTROY(_pubDate);
 
    [super dealloc];
}

これで機械的な繰り返しは消え、よりタイプしやすく、読みやすくなる。「この変数の内容を破棄する」という意図もはっきりする。

メンバ変数とプロパティの命名規則について

特にパターンとして明示されてはいないのだが、この本では「インスタンス変数には _ を接頭辞として付け、_ を取り除いた名前をプロパティ名とする」という規約を採用している。これも大変優れた命名規則で、自分でも真似させてもらったのだが、プロパティをsynthesizeする時の、下のような機械的な繰り返しが例によって我慢ならない。

@synthesize identifier = _identifier;
@synthesize read = _read;
@synthesize title = _title;
@synthesize link = _link;
@synthesize itemDescription = _itemDescription;
@synthesize pubDate = _pubDate;

というわけで次のようなマクロを書いた。

#define PROPVAR(x) x = _ ## x
 
@synthesize
  PROPVAR(identifier),
  PROPVAR(read),
  PROPVAR(title),
  PROPVAR(link),
  PROPVAR(itemDescription),
  PROPVAR(pubDate);

セルのパターン[4]について

カスタムセルのサブビュー、特にUILabelに対して自前でハイライト処理が必要とあるが、実際にはhighlightedプロパティを持つサブビューには自動でハイライト処理が行われるので、自分で処理する必要はない。『Table View Programming Guide for iOS』より引用。

And if the cell is selectable, make sure that the cell content is highlighted appropriately when selected; this happens automatically if the subview implements (if appropriate) the accessor methods for the highlighted property.

iOSでJSONを扱う(YAJL使用)

環境: iOS 4.3, Xcode 4

iOS(iPhoneアプリ)でJSONを扱うライブラリとしてはJSON Frameworkが有名だが、敢えてyajlのObjective-Cバインディングを使ってみた。

インストール

プロジェクトページののREADMEに従ってインストールする。

1. githubのダウンロードページから YAJLiOS-{version}.zip をダウンロードして、展開してできた YAJLiOS.framework ディレクトリを自分のプロジェクトのFrameworksの中にドラッグ&ドロップして追加する。このとき『Copy items into destination group’s folder (if needed)』にチェックを入れておく。

2. XcodeでBuild Settingsを開き、「Other Linker Flags」に -ObjC-all_load の2つのフラグを追加する。

3. JSONを使いたいファイルでヘッダファイルをimportする。

#import <YAJLiOS/YAJL.h>

ドキュメントのインストール

ドキュメントは http://gabriel.github.com/yajl-objc/から参照できるが、Xcodeにインストールすることもできる。

1. githubのダウンロードページからYAJL-{version}.docset.tgzをダウンロードして、展開してできたYAJL.docsetを

~/Library/Developer/Shared/Documentation/DocSets/

の中にコピーする(このディレクトリはデフォルトでは存在しないので、自分で作成する)。

Xcodeを再起動すると、HelpのDocumentationから参照できるようになる。

使い方

JSONのデコード

NSData および NSString として表現されたJSONは yajl_JSON: メソッドで直接デコードできる。

NSError *error;
id result = [data yajl_JSON:&error];
 
if (error) {
    NSLog(@"JSON Parse Error: %@", [error localizedDescription]);
}

エラー引数なしの yajl_JSON の方を使う場合は@try…@catchによるエラー処理を忘れないこと。さもないと不正なJSONをパースしただけでアプリがクラッシュする。

@try {
    result = [data yajl_JSON];
}
@catch (NSException *exception) {
    if ([exception name] == YAJLParserException) {
        NSLog(@"JSON Parse Error: %@", exception);
        result = nil;
    } else {
        @throw;
    }
}

その他、SAXに似たストリーミング形式でのパースも行えるらしい。詳しくはドキュメント参照。

ちなみにarrayやobjectの中の”null”は [NSNull null] に、むき出しの”null”はnilに変換されるようだ。

[[@"[null]" yajl_JSON] objectAtIndex:0] == [NSNull null];
[@"null" yajl_JSON] == nil;

JSONへのエンコード

NSArray, NSDictionary, NSNumber, NSString, NSNull のインスタンスからは yajl_JSONString メソッドで直接JSONにエンコードできる。

NSArray *arr = [NSArray arrayWithObjects:
                @"abc",
                [NSDictionary dictionaryWithObject:[NSNumber numberWithInt:3] forKey:@"key"],
                [NSNull null],
                nil];
NSLog(@"%@", [arr yajl_JSONString]);
//["abc",{"key":3},null]

その他のクラスについては、 -(id)JSON というメソッドを定義して上記5種類のいずれかのインスタンスを返すようにしておけば、やはりyajl_JSONStringでJSON化できる(未定義の場合は例外がthrowされる)。詳しくはドキュメントのYAJLCodingプロトコルの説明を参照。