iPhoneプログラミング(後編)

今回は、本格的なiPhoneアプリを作成してみましょう。基本は(中編)で作成した検索を行うアプリケーションですが、後編は次のことを意識して開発します

  • よりアプリケーションらしく作成する
  • 拡張性を考慮したプログラム構成を意識する

作成するアプリケーションは今回はウエブ検索ではなくヤフーの知恵袋サービスが提供する知恵袋API(質問検索)で、質問が検索できるものを作成します。検索結果をクリックすると、iPhoneらしい画面遷移でWebページ(Q&A画面)が表示されるようにします。中編ではSafariが起動してアプリが終了してしまいましたが、今回はアプリを終了することなく結果が表示されます。

今回はある程度、規模の大きいアプリケーションとなり、作成手順を追って説明するのが難しいのでポイントとなる箇所を各々説明する形をとります。Xcodeで新規プロジェクトを作成しQaSearchBrowser (Navigation-Based Application) というプロジェクト名で開発をスタートしました。最終的なプロジェクトのリソースは

となりました、それぞれのクラスファイルの機能を説明します

QaSearchBrowserAppDelegate Navigation-Based AppのAppDelegate基本クラス(編集する必要なし)
QaSearchBrowserViewController アプリの基本となるクラス. Tableや検索窓を保持する. QaWebViewControllerのインスタンスもこれが保持する
QaWebViewController Sufariの代わりにQ&Aを表示させる画面(UIWebViewを利用している)
QaTableViewCell 今回、検索結果のTableの個々の要素をリッチするために拡張している.それを管理するクラス
QaSearchNextResultsCell 検索結果の追加取得(次の20件)を今回テーブルの自動拡張で機能させている.そのTableの最後の要素を管理するクラス
QaInfo 個々のQ&Aはこのクラスの要素となり、情報を出し入れする
QaList アプリが検索結果として保持しているQ&Aのリストを管理するクラス(Singleton)
RequestQueXML YahooのAPIへリクエストしてXMLを解析するクラス

です、InterfaceBuilderファイルは次の3つです(クリックで別画面で拡大参照できます)

  • MainWindow.xib

  • QaSearchBrowserViewController.xib

  • QaTableViewCell.xib

プログラム(クラス,InterfarceBuilder)は
http://trac.assembla.com/cX0noKTPir3A7Oab7jnrAJ/browser/prog12
にありますので、都度参照しながら以下の解説をみてください。

Point.1 (Q&Aを管理するクラスを作る)

Q&Aは色々な情報を組み合わせて1件を構成しています。プログラム中でQ&A1件を表現するためにそれをクラスかします.QaInfo.hQaInfo.m がそれにあたります。
質問番号(qid),質問文(queString), 質問状態(condition), 回答数(ansCount), 質問日時(postedDate)を内部で保持し、アクセサで代入参照できるようにしています.

Point.2 (アプリ内でどこからでも参照できるQ&Aリストクラスを作成する)

APIから検索結果を取得し保持する、TableのCellに結果を代入するために参照する. などなど、さまざまなプログラム箇所で現時点でアプリで保持しているQ&Aのリスト(個々のQ&AはQaInfoクラス)を参照できるようにできるようにします。これを行うためのテクニックとしてSingletonでクラスを表現する方法がよく用いられます。具体的には以下のようにクラスを作成します. QaList.h

bash-3.2$ cat prog12/Classes/QaList.h
#import <Foundation/Foundation.h>
#import "QaInfo.h"

@interface QaList : NSObject {
	NSMutableArray *list;
	NSInteger totalResults;
}

+ (QaList *)sharedQaList;
- (void)addToQaList:(QaInfo *)newQaInfo;
- (QaInfo *)getQaInfo:(NSInteger)index;
- (void)clearQaList;
- (NSInteger)getQaCount;
- (void)setTotalResults:(NSInteger)hits;
- (NSInteger)getTotalResults;

@end

内部には NSMutableArray でQ&A(QaInfo)を配列として保持します. totalResultsは検索結果件数の情報です
sharedQaListのメソッドがクラスメソッドになっているのがポイントです. implementationの部分は以下になります(ポイントのみ抜粋) QaList.m

bash-3.2$ cat prog12/Classes/QaList.m
...
...
@implementation QaList
static QaList *sharedQaListInstance = nil;

+(QaList *)sharedQaList {
	@synchronized(self) {
		if(sharedQaListInstance == nil) {
			[[self alloc] init];
		}
	}
	return sharedQaListInstance;
}

+ (id)allocWithZone:(NSZone *)zone {
    @synchronized(self) {
        if (sharedQaListInstance == nil) {
            sharedQaListInstance = [super allocWithZone:zone];
            return sharedQaListInstance;
        }
    }
    return nil;
}
...
...
- init {
	if (self = [super init]) {
		list = [[NSMutableArray alloc] init];
	}
	return self;
}
@end

staticで自分自身のインスタンスを保持している部分がポイントです。これを取得するクラスメソッドsharedQaListを用いて例えば、現時点でアプリが保持しているQ&Aの数を調べるには

[[QaList sharedQaList] getQaCount]

Q&Aをリストに追加するには

[[QaList sharedQaList] addToQaList: aQaInfoObject];

とプログラム中どこからでも呼び出せることになります。

Point.3 TableViewCellの表示内容をカスタマイズする

中編ではTableの要素を返すUITableViewのdataSource関数

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 

はUITableViewCellをそのまま用いcell.textに文字列を代入したのみでした、Cellを拡張してカスタマイズすることができます。今回以下のようにカスタマイズされたTableViewCellを作成しました

まずは,TableViewCellの拡張クラス QaTableViewCellを作成します。Xcodeで新規ファイルで UITableViewCell subclassを選択します. QaTableViewCell.hは以下にします

bash-3.2$ cat prog12/Classes/QaTableViewCell.h
#import <UIKit/UIKit.h>
#import "QaInfo.h"

@interface QaTableViewCell : UITableViewCell {
	QaInfo *qaInfo;
	IBOutlet UILabel *lbl_queString;
	IBOutlet UILabel *lbl_postedDate;
	IBOutlet UILabel *lbl_ansCount;
}

@property (nonatomic,retain) QaInfo *qaInfo;

@end

表示に利用する質問文(queString),質問日時(postedDate), 回答数(ansCount)はUILabelでIBOutletとします。QaTableViewCell.mは以下にします.(一部省略)

bash-3.2$ cat prog12/Classes/QaTableViewCell.m
#import "QaTableViewCell.h"

@implementation QaTableViewCell

@synthesize qaInfo;
...
...
...
- (void)setQaInfo:(QaInfo *)_qaInfo {
	if(qaInfo != _qaInfo) {
		[_qaInfo release];
		qaInfo = [_qaInfo retain];
	}
	lbl_queString.text = qaInfo.queString;
	lbl_postedDate.text = qaInfo.postedDate;
	lbl_ansCount.text = [NSString stringWithFormat: @"回答数:%@",qaInfo.ansCount];
}

@end

アクセサを自前で定義してQaInfoが代入された時点でUILabelの各要素のtextを設定する部分setQaInfoも注目してください.
次にInterfaceBuilderで新規xibファイルを作成します(QaTableViewCell.xib). FileからNewを選択したときにTableViewCellはないので、Viewを選択します。その後Viewを削除しLibraryからTableViewCellを選択し交換します.

TableViewCellのClassIdentityを先ほど作成したQaTableViewCellとしたいですが、プロジェクトに属さないと選択できないので、ここでInterfaceBuilderのsaveでQaTableViewCell.xibとして保存しプロジェクトに入れます。そうするとQaTableViewCellがClassとして設定できます。次にCellの上にUILabelを設置しOutletで結びます(QaTableViewCellから設置した各UILabelに結ぶ) UIViewControllerのviewのOutletとUITableViewCellを結ぶのも忘れずに..
.
あともう一つTableViewCellのidentifierを次のようにしておきます

これでTableViewCellの準備は完了です
最後に、TableViewのtableView:cellForRowAtIndexPath:indexPathを次のようにします

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
	static NSString *identifier = @"QaTableViewCell";	
	QaTableViewCell *cell = (QaTableViewCell *)[tableView dequeueReusableCellWithIdentifier:identifier];
	if(cell == nil) {
		UIViewController *controller = [[UIViewController alloc] initWithNibName:identifier bundle:nil];
		cell = (QaTableViewCell *)controller.view;
		[controller release];
	}
	cell.qaInfo = [[QaList sharedQaList] getQaInfo:indexPath.row];
	return cell;
}

Point.4 navigationControllerによるページ切り替え. & UIWebViewの利用方法

今回のアプリでは検索結果をクリックするとページが遷移してウエブページ画面に切り替わるようにします。この部分はnavigationControllerによって実現されます。イメージ的にはnavigationControllerはスタック風にページを管理して次々にページを上に乗せる、または乗せたページを取り除く風の動作をします。乗せる各々ページはUIViewControllerで作ります(UIViewではない)
QaSearchBrowserViewController.xibに(別にxibを作成しても良いが今回はここに作りました) UIViewController(QaWebViewController)を設置しその下にUIViewさらにUIWebViewを置きます。
QaWebViewControllerのクラス定義は QaWebViewController.h, QaWebViewController.mとします。
UIWebViewで指定したURLを開く方法部分は

- (void)loadRequest:(NSString *)qid viewNomalType:(bool)viewNomalType{
	if(viewNomalType) {
		[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q%@",qid]]]];
	} else {
		[webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://chiebukuro.yahoo.co.jp/iphone/detail/q%@",qid]]]];		
	}
}

qidを指定すると指定のQ&Aページが開くようになります。(Yahoo!知恵袋ではiPhoneに最適化されたページとPC用のページがあり引数で出し分けできるようにしました).
検索結果をクリックしたら、このQaWebViewControllerのUIViewControllerをnavigationControllerに設置し画面遷移をさせます。その部分は QaSearchBrowserViewController.m 内の

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
	[webViewController loadRequest:[[[QaList sharedQaList] getQaInfo:indexPath.row] qid] viewNomalType:NO];
	[self.navigationController pushViewController:webViewController animated:YES];
}

で、UIWebViewの画面を切り替えた後, pushViewController で画面を遷移させています。

Point.5 TableViewの動的拡張

iPhoneアプリでよく利用される、Tableの自動拡張によるページ送り(次の○件をクリックするとTableが動的に伸びる形式)を作ってみます。

UITableViewのCellの最後のCellをページ送りをするために準備します. QaSearchBrowserViewController.mのUITableView関連の関数を参照してください. 要素数を応答する際に+1の数を、要素(Cell)を返却する部分では通常のCell(QaTableViewCell)ではなく、遷移用の専用のCell(QaSearchBrowserViewController.xibで定義) QaSearchNextResultsCel を応答します. クリック時の動作時はAPIに再読み込みをさせます. この場合はQaListをクリアせずさらに追記する形でQaListに要素を加えます

最後に

今回はポイントのみを解説したため、順を追った全体的なプログラムのイメージが掴めなかったかもしれませんがプログラムのソースを確認し理解を深めてください。
http://trac.assembla.com/cX0noKTPir3A7Oab7jnrAJ/browser/prog12