昔、SDK4.2のころの記事。twitterのAPIとかもとっくに変わっているかと。
=====
参考:http://www.atmarkit.co.jp/fsmart/articles/iphonesdk04/01.html
目標
TwitterViewerでは非同期通信でTwitterから指定ユーザーのつぶやきを取得する。
Twitterクライアントに必要な機能
- Twitterに接続してデータ(XML)を取得
- 取得したデータ(XML)を解析し、必要な情報を抽出
- 解析結果をiPad/iPhoneに一覧表示
手順
- プロジェクトの作成
- [ファイル]→[新規プロジェクト]で新規プロジェクトウィンドウを表示し、「Navigation-based Application」を選択
「Navigation-based Application」テンプレートには最初から一覧表示に適したUITableViewというクラスが設定されています。
非同期通信を行う「NSURLConnection」クラス
同期通信では、NSURLConnectionを呼び出した側が通信完了まで次の処理に遷移不可。
非同期通信ではNSURLConnectionを呼び出した側は通信完了を待たずに、次の処理に遷移可能。
デメリット:タイミングをずらして後から返ってくる通信結果を受け取る仕組みを作る必要があるため、同期通信に比べ実装量は増える
しかし、ユーザーは通信中に画面のスクロールやキャンセルが行える。ユーザーにとっては非同期通信の方が使いやすいアプリといえる。
新規クラスを作成(URLLoader.m)
NSURLConnectionを利用して非同期通信を行う新規クラスを作成
[Classes]を右クリック→[追加]→[新規ファイル]
インターフェイスファイル(URLLoader.h)
#import <Foundation/Foundation.h> @interface URLLoader : NSObject { NSURLConnection *connection; NSMutableData *data; } @property(retain, nonatomic) NSURLConnection *connection; @property(retain ,nonatomic) NSMutableData *data; - (void) loadFromUrl: (NSString *)url method:(NSString *) method; @end
URLLoder.hは、NSURLConnectionのインスタンスであるconnectionプロパティと、受信したデータを格納するためのdataプロパティを持っています
実装ファイル(URLLoder.m)
@implementation URLLoader @synthesize connection; @synthesize data; // (1) //最初のレスポンス受信時に1回だけ呼ばれる //受信処理の初期化などを行う。 //サンプルでは受信データの格納先であるNSMutableDataインスタンスを初期化しています。 - (void) connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response { self.data = [NSMutableData data]; } // (2) //データ受信時に呼ばれます。 //ここで受け取るデータは受信途中の断片的なデータです。 //通信が終わるまでに何度も呼ばれます。 //そのため、サンプルでは通信が終わるまで フィールドのNSMutableDataに受信データを累積させています。 -(void) connection:(NSURLConnection *)connection didReceiveData:(NSData *)receiveData { [self.data appendData:receiveData]; } // (3) //通信完了時に1回だけ呼ばれます。サンプルで行っている処理については後述します。 - (void) connectionDidFinishLoading:(NSURLConnection *)connection { [[NSNotificationCenter defaultCenter] postNotificationName: @"connectionDidFinishNotification" object: self]; } // (4) //通信エラー発生時に呼ばれます。サンプルで行っている処理については後述します。 - (void) connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { [[NSNotificationCenter defaultCenter] postNotificationName: @"connectionDidFailWithError" object: self]; } // (5) //URLLoaderクラスが、ほかのクラスから利用される際に呼ばれるメソッドです。 //NSURLConnectionインスタンスのデリゲートとしてURLLoaderのインスタンス自身を設定し、初期化しています。 //NSURLConnectionでは、connectionWithRequest:delegateメソッドで初期化を行った場合、特に開始メソッドなどを呼ばなくても、 非同期通信が開始します。 - (void) loadFromUrl: (NSString *)url method: (NSString *) method { NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; [req setHTTPMethod:method]; self.connection = [NSURLConnection connectionWithRequest:req delegate:self]; } - (void)dealloc { [connection release]; [data release]; [super dealloc]; } @end
(1)~(4)はNSURLConnectionのデリゲート(委譲)処理です。NSURLConnectionのデリゲートプロトコルであるNSURLConnectionDelegateは、NSObjectの非形式プロトコルとして宣言されています。このため、NSObjectのサブクラスであるURLLoaderにデリゲートメソッドを実装するだけでNSURLConnectionのデリゲートとして機能します。
呼び出し
RootViewControllerから呼び出し
RootViewController.m
#import "URLLoader.h" // (6) //loadTimeLineByUserNameメソッドでは、タイムライン取得開始処理 // @"http://twitter.com/status/user_timeline/%@.xml"; //指定したユーザーのタイムラインをXMLで取得するためのTwitterが提供しているURLフォーマット //「%@」部分を取得したいユーザーのユーザー名に変えてリクエストを送ると、そのユーザーのタイムラインがXMLで取得できます。 - (void) loadTimeLineByUserName: (NSString *) userName { static NSString *urlFormat = @"http://twitter.com/status/user_timeline/%@.xml"; NSString *url = [NSString stringWithFormat:urlFormat, userName]; //URLLoaderクラスを初期化 URLLoader *loder = [[[URLLoader alloc] init] autorelease]; //Cocoaフレームワークの「Cocoa Notification」という仕組みを使ってURLLoaderの通信完了通知を受け取るよう指定 //引数objectに指定されたURLLoaderのインスタンスloaderから「connectionDidFinishNotification」という名前の通知があった場 合に、 //「loadTimeLineDidEnd」というメソッドが実行されるようにNotificationCenterに登録 [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(loadTimeLineDidEnd:) name: @"connectionDidFinishNotification" object: loder]; [[NSNotificationCenter defaultCenter] addObserver: self selector: @selector(loadTimeLineFailed:) name: @"connectionDidFailWithError" object: loder]; [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; [loder loadFromUrl:url method: @"GET"]; } // (7) - (void) loadTimeLineDidEnd: (NSNotification *)notification { URLLoader *loder = (URLLoader *)[notification object]; NSData *xmlData = loder.data; NSLog(@"%@", [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding]); } // (8) - (void) loadTimeLineFailed: (NSNotification *)notification { UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"エラー" message:@"タイムラインの取得に失敗しました。" delegate:self cancelButtonTitle:@"閉じる" otherButtonTitles:nil]; [alert show]; [alert release]; }
iOS SDK 4のシミュレータを使う
RootViewController.mのviewDidLoadメソッドのコメントアウトを外し、viewDidLoadメソッド内でloadTimeLineByUserNameメソッドを呼び出すように編集
RootViewController.m
- (void)viewDidLoad { [super viewDidLoad]; [self loadTimeLineByUserName:@"itmedia"]; }
Twitterユーザー名である「itmedia」を引数に渡してloadTimeLineByUserNameメソッドを呼び出しました
Xcodeの[実行]→[コンソール]をクリックしてコンソールを表示
シミュレータを起動して実行し、以下のようにコンソールにXMLが表示されればデータの取得は成功
XMLを解析する「NSXMLParser」クラス
NSURLConnectionクラス使って取得したXMLには、多くの情報が含まれています。今回はその中から、「ユーザー名」と「つぶやき」を取得します。このようなXMLの解析とデータ変換のためにCocoa Touch フレームワークにはNSXMLParserクラスが用意されています。
一般に、XML解析にはDOM(Document Object Model)とSAX(Simple API for XML)という2種類の方式があります。NSXMLParserはSAX型のXML解析クラスです。SAXでは、XMLを上から順に読んでいき、「開始タグが見つかった」「終了タグに到達した」などのイベントごとに処理します。
クラスを作成
[Classes]を右クリック→[追加]→[新規ファイル]で、[新規ファイル]ウィンドウを開いたら、[Objective-C class]、[Subclass of]に[NSObject]を選択して[次へ]をクリックします。
[ファイル名]を「StatusXMLParser.m」として[完了]をクリックします。
インターフェイスファイル(StatusXMLParser.h)
StatusXMLParser.h
#import <Foundation/Foundation.h>
@interface StatusXMLParser : NSObject <NSXMLParserDelegate> { NSMutableString *currentXpath; NSMutableArray *statuses; NSMutableDictionary *currentStatus; NSMutableString *textNodeCharacters; } @property (retain , nonatomic) NSMutableString *currentXpath; @property (retain , nonatomic) NSMutableArray *statuses; @property (retain , nonatomic) NSMutableDictionary *currentStatus; @property (retain , nonatomic) NSMutableString *textNodeCharacters; - (NSArray *) parseStatuses: (NSData *) xmlData; @end
NSXMLParserは「開始タグが見つかった」などのイベントごとにデリゲートに処理を委譲します。 このためStatusXMLParser.hでは、NSXMLParserのデリゲートになれるよう<NSXMLParserDelegate>プロトコルを採用しています。
実装ファイル(StatusXMLParser.m)
StatusXMLParser.m
@implementation StatusXMLParser @synthesize currentXpath; @synthesize statuses; @synthesize currentStatus; @synthesize textNodeCharacters; // (9) //ドキュメントが開始したとき呼ばれる - (void) parserDidStartDocument:(NSXMLParser *)parser { self.currentXpath = [[[NSMutableString alloc]init] autorelease]; self.statuses = [[[NSMutableArray alloc] init] autorelease]; } // (10) //要素が開始した(開始タグが見つかった)とき呼ばれる - (void) parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qua lifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { [self.currentXpath appendString: elementName]; [self.currentXpath appendString: @"/"]; self.textNodeCharacters = [[[NSMutableString alloc] init] autorelease]; if ([self.currentXpath isEqualToString: @"statuses/status/"]) { self.currentStatus = [[[NSMutableDictionary alloc] init] autorelease]; } } // (11) //要素が終了した(終了タグが見つかった)とき呼ばれる - (void) parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI quali fiedName:(NSString *)qName { NSString *textData = [self.textNodeCharacters stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineC haracterSet]]; if ([self.currentXpath isEqualToString: @"statuses/status/"]) { [self.statuses addObject:self.currentStatus]; self.currentStatus = nil; } else if ([self.currentXpath isEqualToString: @"statuses/status/text/"]) { [self.currentStatus setValue:textData forKey:@"text"]; } else if ([self.currentXpath isEqualToString: @"statuses/status/user/name/"]) { [self.currentStatus setValue:textData forKey:@"name"]; } int delLength = [elementName length] + 1; int delIndex = [self.currentXpath length] - delLength; [self.currentXpath deleteCharactersInRange:NSMakeRange(delIndex,delLength)]; } // (12) //要素の中で文字列が見つかったとき呼ばれる - (void) parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { [self.textNodeCharacters appendString:string]; } // (13) //StatusXMLParserを利用するクラスから呼ばれるメソッドです。 //NSXMLParserインスタンスのデリゲートにStatusXMLParserインスタンス自身をに設定して初期化し、解析処理をスタートします。 //StatusXMLParserでは現在解析中のXML要素をパス形式の文字列としてcurrentXpathプロパティで管理しています。 //要素が終了した(終了タグが見つかった)ときに、 //currentXpathプロパティが、「statuses/status/text/」だった場合は「つぶやき」として、 //「statuses/status/user/name/」だった場合は「ユーザー名」として、 //一時データ格納先のcurrentStatusプロパティにテキスト値であるtextNodeCharactersプロパティの値を設定 //そして、1つの<status>要素が終了したらcurrentStatusプロパティの中身をstatusesプロパティに追加 - (NSArray *) parseStatuses:(NSData *)xmlData { NSXMLParser *parser = [[[NSXMLParser alloc] initWithData:xmlData] autorelease]; [parser setDelegate:self]; [parser parse]; return self.statuses; } - (void) dealloc { [currentXpath release]; [statuses release]; [currentStatus release]; [textNodeCharacters release]; [super dealloc]; } @end
(9)~(12)がXMLParserDelegateプロトコルに準拠したデリゲートメソッドです。それぞれXML解析時に以下のタイミングで呼び出されます。
データをUITableViewに一覧表示する
※StatusXMLParserクラスをRootViewControllerクラスから呼び出して使用
RootViewController.hを編集
#import <UIKit/UIKit.h> @interface RootViewController : UITableViewController { NSArray *statuses; } @property(retain, nonatomic) NSArray *statuses; @end
RootViewController.hには解析済みのデータを保存するstatusesプロパティを追加
実装ファイルのRootViewController.mにStatusXMLParser.hをインポート、プロパティの実装も宣言
#import "StatusXMLParser.h" @synthesize statuses
loadTimeLineDidEndメソッドを次のように書き換え
- (void) loadTimeLineDidEnd: (NSNotification *)notification { URLLoader *loder = (URLLoader *)[notification object]; NSData *xmlData = loder.data; StatusXMLParser *parser = [[[StatusXMLParser alloc] init] autorelease]; self.statuses = [parser parseStatuses:xmlData]; [self.tableView reloadData]; }
ログ出力処理の代わりにStatusXMLParserを呼び出し、解析した結果をstatusesに設定 その後Viewの更新処理を読び出し
// (14) //表示行数を設定するためメソッドです。 //今回は、statusesプロパティの持つデータの数を返しています。 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [self.statuses count]; } //(15) //UITableViewの1つ1つのセルをどのように表示するかの設定を行うメソッドです。 //statusesプロパティから「ユーザー名」「つぶやき」を取得し、それぞれラベルに設定しています。 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleValue2 reuseIdentifier:CellIdentifier] autor elease]; } int row = [indexPath row]; NSString *name = [[statuses objectAtIndex:row] objectForKey:@"name"]; NSString *text = [[statuses objectAtIndex:row] objectForKey:@"text"]; // ユーザー名 cell.textLabel.font = [UIFont systemFontOfSize:16]; cell.textLabel.textColor = [UIColor lightGrayColor]; cell.textLabel.text = name; // テキスト cell.detailTextLabel.numberOfLines = 0; cell.detailTextLabel.font = [UIFont systemFontOfSize:14]; cell.detailTextLabel.text = text; return cell; }
「Navigation-based Application」テンプレートから作られたRootViewControllerクラスはUITableViewControllerというクラスを継承して作られており、この親クラスがもつデリゲートメソッドを上書きしてUITableViewの表示をコントロールできます。
行の高さを調節 RootViewController.m 以下を追加
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath { UITableViewCell *cell = [self tableView:self.tableView cellForRowAtIndexPath:indexPath]; //下記の個所でつぶやき表示領域のサイズを取得 //横幅に関しては、「テーブルの横幅 - ユーザー名のテキストラベルの横幅」とするのが望ましいのでしょうが、 //この時点ではユーザー名のテキストラベルの横幅も未設定なため大方の値として150と指定しています。 CGSize bounds = CGSizeMake(self.tableView.frame.size.width - 150, self.tableView.frame.size.height); //cell.detailTextLabel.textの「sizeWithFont: constrainedToSize: lineBreakMode」メソッドを呼び出し、 //文字の長さに応じた表示サイズを取得して返しています。 CGSize size = [cell.detailTextLabel.text sizeWithFont: cell.detailTextLabel.font constrainedToSize: bounds lineBreakMode: UILineBreakModeCharacterWrap]; return size.height; }
固定の高さではなく、表示領域の横幅と文字の長さに応じた高さを取得して返すようにしています。
ナビゲーションバーに更新ボタンを追加する
画面上につぶやきの更新ボタンを配置 ※Interface Builderではなく、ソース上で
RootVIewController.mに更新用のメソッド、reloadを追加
- (void) reload: (id)sender { [self loadTimeLineByUserName:@"itmedia"]; }
viewDidLoadメソッドを編集し、更新ボタンの追加し、その更新ボタンが押されたら、reloadメソッドが呼ばれるようにします
ついでに、ナビゲーションバーの色もTwitterのイメージカラーに変更
- (void)viewDidLoad { [super viewDidLoad]; // ナビゲーションバーの色を青っぽく変更 self.navigationController.navigationBar.tintColor = [UIColor colorWithRed:0.3 green:0.6 blue:0.7 alpha:1.0]; // 更新ボタンの追加 self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemRefresh target:self action:@selector(reload:)]; [self loadTimeLineByUserName:@"itmedia"]; }
networkActivityIndicatorで通信中であることを知らせる
通信中であることを表すnetworkActivityIndicatorを追加 ステータスバーに回転する画像を表示する機能 ソースコード中のフラグの切り替えだけで簡単に表示・非表示を設定
RootVIewController.m のloadTimeLineByUserName: userNameメソッド内の通信開始直前でnetworkActivityIndicatorVisibleプロパティを「YES」にすると、回転画像が表示されます。
……【省略】……
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES; [loder loadFromUrl:url method: @"GET"];
}
通信完了後は、loadTimeLineDidEndの先頭でnetworkActivityIndicatorVisibleを「NO」にすることで回転画像が非表示となります
- (void) loadTimeLineDidEnd: (NSNotification *)notification {
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
……【省略】…… }
以上で完成
WebサービスをiPhone/iPadから利用する際の基本
今回紹介した非同期通信によるデータの取得・XML解析は、TwitterだけでなくさまざまなWebサービスをiPhone/iPadから利用する際にも基本となる技術です。これらを用いることで作成できるiPhone/iPadアプリの幅がぐっと広がるのではないでしょうか。
コメント