忍者ブログ
プログラミングとか日常とかの覚書っぽいなにか
[27] [26] [25] [24] [23] [22] [21] [20] [19] [18] [17]
×

[PR]上記の広告は3ヶ月以上新規記事投稿のないブログに表示されています。新しい記事を書く事で広告が消えます。

iPhone用Twitterクライアント作成中、今回は自分のタイムライン(home_timeline)の取得を行います。

今回は、まだ説明をしていない以下のライブラリを使うことになります。
  • GTMHTTPFetcher
  • SBJson (json-framework)

(2012/06/19)
iOS5/ARC対応版の記事を書きましたので、そちらを参照してください。この記事の内容は古いです。

GTMHTTPFetcher によるHTTP要求

Twitter APIは、HTTPによる通信を利用して情報のやり取りを行うRESTと呼ばれる方式を採用しています。

一般にiOSプログラミングでのHTTP通信は、 NSURLRequest (NSMutableURLRequest) オブジェクトにURLやパラメータを含む要求情報をセットし、これを渡して NSURLConnection オブジェクトを生成するという手続きを取ります。

NSURLConnection での通信の方式には、同期通信と非同期通信の2通りがあります。

同期通信は、応答データを取得できるまで、その場で処理を止めてずっと待ち続ける方式です。この場合、応答データを受信するか、エラーで中断されるかしないと、ずっとそこで処理を停止し続ける状態になります。そうなると画面も固まった状態になってしまうため、作るのは簡単ではあるけれども、推奨される方法ではありません。

一方、非同期通信は、とりあえず要求だけしておくと、自動的に裏で受信データを待機し、データを受け取ったらアプリケーション側に通知(コールバック)する仕組みになっています。データを待っている間、アプリケーションは他の処理を行うことができるので、画面が固まったりすることもなくなります。ここでも非同期通信の方式で進めていくことにします。

ただし、ここでの非同期通信には NSURLConnection の代わりに GTMHTTPFetcher を使います。
GTMHTTPFetcher は NSURLConnection のラッパークラスという位置付けですが、リダイレクト制御や再送処理などといった NSURLConnection にはない付加機能も提供されています。
ただ、そういった付加機能は実はどうでもよくて(いや、どうでもいいというのは言い過ぎですが…)、 NSURLConnection を使う場合にアプリケーションが行わなくてはならないさまざまな処理を単純化してくれるのが特徴です。また、コールバック先のメソッドをセレクタで指定できるというのも魅力的です。

NSURLConnection で非同期通信を行う場合、デリゲートとなるアプリ側はまず最初の応答受信で connection:didReceiveResponse: を、応答データ受信時に connection:didReceiveData: を、通信完了時に connectionDidFinishLoading: を、そしてエラー時に connection:didFailWithError: を受け取りますが、これらを全部考えるのはあまりにも面倒です。

また、複数の種類の要求に対して同じデリゲートオブジェクトが通知を受ける場合、例えば今回のTwitterクライアントを考えるとして、home_timelineの取得要求、user_timelineの取得要求、タイムライン更新要求など複数の種類の要求をすべて同一のビューコントローラで行いたい場合(設計としてはアレな気もしますがそこは気にしない)、どの種類の要求に対する応答でも、同じデリゲートメソッドが呼び出されてしまいます。

もし NSURLConnection を使おうとすると、以下のような面倒なコーディングをすることになることでしょう。

// タイムライン要求
- (void)requestHomeTimeline
{
    // 要求作成
    NSMutableURLRequest *request = ...(省略)...

	// 接続を生成 (後で比較するためメンバ変数に保持)
	homeTimelineConnection_ = [NSURLConnection connectionWithRequest:request
                                                            delegate:self];
}

// ツイート投稿要求
- (void)requestTweet
{
    // 要求作成
    NSMutableURLRequest *request = ...(省略)...

	// 接続を生成 (後で比較するためメンバ変数に保持)
	tweetConnection_ = [NSURLConnection connectionWithRequest:request
                                                     delegate:self];
}

// 応答受信時、応答データ受信前の処理
-  (void)connection:(NSURLConnection *)connection
 didReceiveResponse:(NSURLResponse *)response
{
    if ([connection isEqual:homeTimelineConnection_]) {
        // タイムライン要求に対する応答の場合
        // タイムライン取得用データ受信バッファを初期化する
        [timelineRecvData_ setLength:0];

    } else if ([connection isEqual:tweetConnection_]) {
        // ツイート投稿要求に対する応答の場合
        // ツイート結果用データ受信バッファを初期化する
        // (応答データ(ツイートしたstatus情報)を使わないなら特に何もしない)
        ... (省略) ...
    }
}

// 応答データ受信時の処理
- (void)connection:(NSURLConnection *)connection
    didReceiveData:(NSData *)data
{
    if ([connection isEqual:homeTimelineConnection_]) {
        // タイムライン要求に対する応答の場合
        // データをタイムライン取得用データ受信バッファ末尾に追加
        [timelineRecvData_ appendData:data];

    } else if ([connection isEqual:tweetConnection_]) {
        // データをツイート投稿要求に対する応答の場合
        // 応答データをツイート結果用データ受信バッファに追加
        // (応答データ(ツイートしたstatus情報)を使わないなら特に何もしない)
        ... (省略) ...
    }
}

// 全応答データ受信完了時の処理
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    if ([connection isEqual:homeTimelineConnection_]) {
        // home_timelne要求に対する応答の場合
        // タイムライン取得用データ受信バッファのデータを解析
        ... (省略) ...

    } else if ([connection isEqual:tweetConnection_]) {
        // ツイート投稿要求に対する応答の場合
        // ツイート結果用データ受信バッファのデータを解析
        // (応答データ(ツイートしたstatus情報)を使わないならツイート完了後処理を行う)
        ... (省略) ...
    }
}

// エラー発生時の処理
- (void)connection:(NSURLConnection *)connection
  didFailWithError:(NSError *)error
{
    if ([connection isEqual:homeTimelineConnection_]) {
        // home_timelne要求に対する応答の場合
        // タイムライン取得エラー時のエラー処理
        ... (省略) ...

    } else if ([connection isEqual:tweetConnection_]) {
        // ツイート投稿要求に対する応答の場合
        // ツイート投稿時のエラー処理
        ... (省略) ...
    }
}

このような構造では、応答データを受け取っても、いったいどの要求に対する応答なのか判断が面倒ですし、そのための処理を書こうとすると、流れがつかみにくくなってしまいます。複数の要求を同時に行いたい場合にはさらに慎重に設計をする必要が出てきます。

一方、 GTMHTTPFetcher では面倒な処理は内部でやってくれて、応答をすべて受信するか、エラーが発生するかして通信が終了したときに、要求時に指定したセレクタのメソッドが呼び出されるようになっています。
GTMHTTPFetcher では、以下のシグネチャを持つメソッドを応答時のコールバック先として指定できます。

- (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;

シグネチャ(戻り値の型と、引数の数とそれぞれの型の組)が一致していればメソッド名は何でもいいので、要求の種類に応じて別々のメソッドをコールバック先に指定することができるのです。

上記のコードと同じ処理を GTMHTTPFetcher で行うとすると、以下のようになると思います。

// タイムライン要求
- (void)fetchGetHomeTimeline
{
    // 要求作成
    NSMutableURLRequest *request = ...省略...

	// 接続開始
    GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(homeTimelineFetcher:finishedWithData:error:)];
}

// タイムライン要求に対する応答
- (void)homeTimelineFetcher:(GTMHTTPFetcher *)fetcher
           finishedWithData:(NSData *)data
                      error:(NSError *)error
{
    if (error != nil) {
        // タイムライン要求時にエラー発生
        // タイムライン取得エラー時のエラー処理
        ... (省略) ...

    } else {
        // タイムラインを正常に受信完了
        // タイムライン取得用データ受信バッファのデータを解析
        ... (省略) ...
    }
}

// ツイート投稿要求
- (void)fetchPostTweet
{
    // 要求作成
    NSMutableURLRequest *request = ...省略...

	// 接続開始
    GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
    [fetcher beginFetchWithDelegate:self
                  didFinishSelector:@selector(postTweetFetcher:finishedWithData:error:)];
}

// ツイート投稿要求に対する応答
- (void)postTweetFetcher:(GTMHTTPFetcher *)fetcher
        finishedWithData:(NSData *)data
                   error:(NSError *)error
{
    if (error != nil) {
        // ツイート投稿要求時にエラー発生
        ... (省略) ...
    } else {
        // ツイート投稿を正常に完了
        ... (省略) ...
    }
}

要求の種類ごとに応答時のコールバックを受け取るメソッドが分かれているので、上記の NSURLConnection を直接扱う場合と比べると、処理の流れがわかりやすくなっていますし、それぞれのエラー判定も簡単です。


SBJson (json-framework) による応答データの解析

Twitter APIを使用したときの応答データは、XMLやJSONなど複数の形式から選択することができますが、今回はJSONを選択することにしました。
というのも、iOS SDKにはXMLを解析するための NSXMLParser クラスが標準で用意されていますが、それを使用してXML形式の応答データを解析するよりも、SBJsonライブラリを使ってJSON形式の応答データを解析する方がずっと簡単だからです。
また、JSON形式はXML形式と比べ、解析にかかる時間や、サーバや回線への負荷が比較的が小さいという利点もあるようです。

GTMHTTPFetcher を使用する場合、コールバック先のメソッドは以下の形式で応答データを受け取ります。

- (void)fetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error;

エラーが発生していなければ、引数 data が応答データを持ちます。要求時にJSON形式で取得するように指定した場合には、このデータがJSON形式になっています。

SBJsonライブラリはJSON形式の文字列データを解析してデータを返します。返されるデータは NSDictionary だったり NSArray だったり NSString だったりと様々です。元のJSONの値の形式によって、以下の型が返されるようです。
  • Null値 → NSNull
  • 文字列 → NSString (NSMutableString)
  • Array構造 → NSArray (NSMutableArray)
  • Object構造 → NSDictionary (NSMutableDictionary)
  • Bool値 → NSNumber (initWithBool: で初期化)
  • 数値 → NSNumber または NSDecimalNumber

例えばTwitterのタイムライン情報(home_timelineなど)を受け取った場合、最上位がステータス情報(status)の配列(NSArray)になっており、その配列の個々の要素はステータス情報を保持する NSDictionary 型の辞書です。この辞書では、例えばキー "text" に対する値がツイート本文になっていたりします。また、キー "user" に対する値は、ツイートしたユーザーの情報を含む NSDictionary になっています(ちょっとややこしい)。

どのような構造のデータを返すかはTwitter APIごとに異なります。それぞれのTwitter APIが返す応答の構造について詳しく知りたい場合には、Twitter APIのリファレンスを参照してください。


次に、SBJsonライブラリを使用してJSON形式の文字列情報を含む NSData オブジェクトを解析する方法を説明していきましょう。

JSONライブラリではカテゴリを使用して NSString に追加された JSONValue メソッドを使用してJSON形式の文字列を解析できるようになっています。そこで、まず NSData オブジェクトから NSString を作成し、その NSString オブジェクトの JSONValue を呼び出すことで解析(パース)されたデータ構造を取得することができます。

- (void)homeTimelineFetcher:(GTMHTTPFetcher *)fetcher
           finishedWithData:(NSData *)data
                      error:(NSError *)error
{
    // NSData から NSString を生成
    NSString *jsonStr = [[[NSString alloc] initWithData:data
                                               encoding:NSUTF8StringEncoding] autorelease];

    // JSON	形式の文字列を解析 (パース)
    NSArray *statuses = [jsonStr JSONValue];

    ... 省略 ...
}

JSONValue は内部的には SBJsonParser クラスを使用していますが、この SBJsonParser を直接使用することもできます。この場合は objectWithData メソッドを使用することで、以下の通り NSData から直接パースを行うことができます。今回はこちらの方法を使うことにします。

- (void)homeTimelineFetcher:(GTMHTTPFetcher *)fetcher
           finishedWithData:(NSData *)data
                      error:(NSError *)error
{
    // JSON	パーサーを生成
    SBJsonParser *parser = [[[SBJsonParser alloc] init] autorelease];

    // JSON	形式文字列を持つ NSData オブジェクトを解析 (パース)
    NSArray *statuses = [parser objectWithData:data];

    ... 省略 ...
}


タイムライン情報の取得

では今までのことを踏まえて、タイムライン(home_timeline)の取得を行ってみます。

まずはタイムライン取得要求をして応答を受信し、JSONデータの解析をして内部で保持するまでの部分です。
JSON形式で受信される情報の解析後のタイムライン情報を、とりあえずそのまま NSArray のメンバ変数 timelineStatuses_ に保持しています。

// デフォルトのタイムライン表示処理
- (void)showHome
{
    [self fetchGetHomeTimeline];
}

// タイムライン(home_timeline)取得
- (void)fetchGetHomeTimeline
{
    // 要求を準備
    NSURL *url = [NSURL URLWithString:@"http://api.twitter.com/1/statuses/home_timeline.json"];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setHTTPMethod:@"GET"];

    // 要求に署名情報を付加
    [auth_ authorizeRequest:request];

    // 非同期通信による取得開始
    GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
    [fetcher beginFetchWithDelegate:self didFinishSelector:@selector(homeTimelineFetcher:finishedWithData:error:)];
}

// タイムライン(home_timeline)取得応答時
- (void)homeTimelineFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error
{
    if (error != nil) {
        // タイムライン取得エラー
        NSLog(@"Fetching statuses/home_timeline error: %d", error.code);
        return;
    }

    // タイムライン取得成功
    // JSONデータをパース
    SBJsonParser *parser = [[[SBJsonParser alloc] init] autorelease];
    NSArray *statuses = [parser objectWithData:data];

    // 前の情報があれば破棄
    [timelineStatuses_ autorelease];

    // タイムライン情報(NSArray)をそのまま保持
    timelineStatuses_ = [statuses retain];

    // テーブルを更新
    [self.tableView reloadData];
}

取得エラー時に何もしていませんが、実際にはAlertを表示するくらいはした方が良いかもしれません。

正常に受信できたら NSArray 形式のタイムライン情報を retain してメンバに保持します。その後に UITableView の reloadData を呼ぶことにより、テーブル表示を再構築しています。

テーブル表示に関わる部分は以下の通り。

// テーブルのセクション数
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

// テーブルの行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [timelineStatuses_ count];
}

// テーブルの指定位置に挿入されるセルの要求
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle
                                       reuseIdentifier:CellIdentifier] autorelease];
    }

    // 対象インデックスのステータス情報を取り出す
    NSDictionary *status = [timelineStatuses_ objectAtIndex:indexPath.row];

    // ツイート本文を表示
    cell.textLabel.numberOfLines = 0;
    cell.textLabel.font = [UIFont systemFontOfSize:12];
    cell.textLabel.text = [status objectForKey:@"text"];
    
    // ユーザ情報から screen_name を取得して表示
    NSDictionary *user = [status objectForKey:@"user"];
    cell.detailTextLabel.text = [user objectForKey:@"screen_name"];

    return cell;
}

// 指定位置の行で使用する高さの要求
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSDictionary *status = [timelineStatuses_ objectAtIndex:indexPath.row];
    NSString *content = [status objectForKey:@"text"];
    CGSize labelSize = [content sizeWithFont:[UIFont systemFontOfSize:12]
                           constrainedToSize:CGSizeMake(300, 1000)
                               lineBreakMode:UILineBreakModeWordWrap];
    return labelSize.height + 25;
}


本文と名前(screen_name)のみで、投稿時刻やらアイコンやらはまったく表示しないですが、ひとまずはタイムラインが表示までできたということにして、今回はここまでとします。

拍手

PR

コメント


コメントフォーム
お名前
タイトル
文字色
メールアドレス
URL
コメント
パスワード
  Vodafone絵文字 i-mode絵文字 Ezweb絵文字


忍者ブログ [PR]
プロフィール
HN:
はむぱい
職業:
ソフト作ったりしてる人
Twitter
最新CM
[06/09 replica rolex oyster perpetual datejust]
[06/09 bracelets imitation cartier love]
[06/09 replica the oyster perpetual datejust]
[06/09 datejust rolex oyster perpetual]
[06/09 replica gold love bangle]
カレンダー
02 2024/03 04
S M T W T F S
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
ブログ内検索
あ~いい漢字