忍者ブログ
プログラミングとか日常とかの覚書っぽいなにか
[39] [38] [37] [36] [35] [34] [33] [32] [31] [30] [29]
×

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

前回に引き続きGTMOAuthライブラリを使用したiOS5用Twitterクライアント作成の解説です。
今回はTwitterへのサインイン(ここではOAuth認証・認可の手続き)を進めたいと思います。

今回は、まだ説明をしていない GTMHTTPFetcher ライブラリを使うことになります。

また、JSON形式データの解析には、iOS5以降でサポートされるようになった NSJSONSerialization クラスを使用します。iOS4では使用できないので、どうしても古いバージョンのiOSでも動作するようにしたい場合には、SBJsonなどのライブラリに挑戦してみてください。


GTMHTTPFetcher によるHTTP要求

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

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

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

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

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

ただし、ここでの非同期通信には 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 を直接扱うコードと比べると、処理の流れがわかりやすくなっていますし、それぞれのエラー判定も簡単です。


NSJSONSerialization による応答データ解析

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


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

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

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

NSJSONSerialization クラスはJSON形式の文字列データと、実際の配列(NSArray)または辞書(NSDictionary)データとを相互に変換する機能を持っています。今回はJSON文字列データから配列データを行うので、JSONObjectWithData:options:error: クラスメソッドを使用します。このメソッドから返されるデータは、トップレベルが NSDictionary または NSArray であり、その子要素は含まれるデータの型に応じて NSString, NSNumber, NSDictionary, NSArray, NSNull となります。

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

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

A field guide to Twitter Platform objects - Twitter Developers
https://dev.twitter.com/docs/platform-objects

Tweets - Twitter Developers (ツイート情報に含まれる内容)
https://dev.twitter.com/docs/platform-objects/tweets

応答データがJSON形式のデータなので、NSJSONSerialization クラスの JSONObjectWithData クラスメソッドにそのまま渡します。すると、戻り値としてツイート情報を要素とする配列 (NSArray) が返されます。
- (void)homeTimelineFetcher:(GTMHTTPFetcher *)fetcher
           finishedWithData:(NSData *)data
                      error:(NSError *)error
{
    // NSData に含まれるJSONデータを NSArray に変換
    NSError *jsonError = nil;
    NSArray *statuses = [NSJSONSerialization JSONObjectWithData:data
                                                        options:0
                                                          error:&jsonError];

    ... 省略 ...
}

タイムライン情報の取得

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

まず、ツイート情報の配列をインスタンス変数として保持しておきたいので、 @implementation の下にインスタンス変数の宣言を追加しておきます。ここでは変数名を timelineStatuses_ としています。
@implementation MasterViewController {
    // OAuth認証管理
    GTMOAuthAuthentication *auth_;

    // 表示中ツイート情報
    NSArray *timelineStatuses_;
}

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

// デフォルトのタイムライン処理表示
- (void)asyncShowHomeTimeline
{
    [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 status/home_timeline error: %d", error.code);
        return;
    }

    // タイムライン取得成功
    // JSONデータをパース
    NSError *jsonError = nil;
    NSArray *statuses = [NSJSONSerialization JSONObjectWithData:data
                                                        options:0
                                                          error:&jsonError];

    // JSONデータのパースエラー
    if (statuses == nil) {
        NSLog(@"JSON Parser error: %d", jsonError.code);
        return;
    }

    // データを保持
    timelineStatuses_ = statuses;

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

取得エラー時にログを残す以外に何もしていませんが、実際にはAlertを表示してユーザに通知するくらいはした方が良いかもしれません。

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


タイムライン情報のテーブルビュー表示

最後に、取得したタイムラインをテーブルビューに表示するコードを追加していきます。

まず、テーブルビューのセルのスタイルを「Subtitle」設定します。
Storyboardを使用して画面部分を作成しているので、セルのスタイルもStoryboardから変更します。

プロジェクトビューの「MainStoryboard.storyboard」を選択して、Storyboardを表示し、Master View Controllerの配下にある「Table View Cell - Cell」を選択します(画面イメージ上のテーブルビューのセル部分を直接クリックして選択することも可能)。



Cellが選択された状態でAttributes Inspectorを表示し(ショートカットキー:option+command+4)、Table View CellのStyleを「Subtitle」に変更します。また、Accessoryは「None」にしておきます。

また、プロジェクト作成時のデフォルトの状態ではテーブルビューのセルをクリックするとDetail画面に遷移するようになっていますが、ここでは必要ないので、「Push Segue from Cell to Detail」を選択してDeleteキーで削除しておきます。

テーブル表示に関わるコードを以下の通り書いていきます。

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

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

// 指定位置に挿入されるセルの要求
- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];

    // 対象インデックスのステータス情報を取り出す
    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;
}

最後の tableView:heightForRowAtIndexPath: メソッドで、ツイート本文の描画サイズをもとにしてセルの高さを返すことで、セルの高さが自動的に調整されるようにしてみました。 以上で、サインインしたユーザのタイムライン(home_timeline)がテーブルビューに表示されるようになりました。



現在のソースではいったん表示されたらそのままですが、タイマーを使って定期的に更新したりなど、改善の余地はいくらでもあると思いますので、いろいろと挑戦してみてください。

拍手

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]
カレンダー
10 2024/11 12
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
ブログ内検索
あ~いい漢字