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.h と QaInfo.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
iPhoneプログラミング(中編)
今回は,他の演習と同じようにヤフーの検索APIを利用したiPhoneアプリを作成してみます。検索窓と検索結果がある画面で、キーワードを入力すると検索結果が表示されます。検索結果をタップするとSafariが起動してサイトが閲覧できる簡単なアプリです。
今回のプログラムは説明と理解の難易度を下げるためにあえて、モジュール分割をせずコーディングしています。この状態で拡張していくとあるいみ破綻する可能性がありますので、開発理解を深めるためのコードと割り切って閲覧下さい。
それでは、前回の前編と同様にXcodeを起動し「新規プロジェクト」を作成します。今回は「WebSearch」というプロジェクト名にしました。自動生成されたプログラムファイルは以下のようになっていると思います。
今回この中で修正するファイルは
- WebSearchViewController.h
- WebSearchViewController.m
- WebSearchViewController.xib (Interface Builderで編集)
この3ファイルのみです。まずInterface Builder(以下IB)での作業から行います。 WebSearchViewController.xibをダブルクリックしIBを起動します. Search Bar と Table View の2つを Libraryから Viewに以下のように設置します。
この状態でビルド実行してみると検索窓とテーブルが表示されます。SearchBarをクリックするとキーボードが表示され、キーワードがSearchBarに表示されますが、検索ボタンを押しても何も反応しません。
まずは Search Barの動きを確認してみましょう. XcodeワークスペースガイドのiPhoneOS2.2LibraryからUISearchBarを調べてみるとUISearchBarDelegateがあることが分かります。そのDelegateを見てみると
Sent to the delegate when the search button is tapped. - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
というメソッドがあります。これを使えば良さそうです。さて、この関数を WebSearchViewController.m に追記します
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { NSLog(searchBar.text); [searchBar resignFirstResponder]; }
これだけでは、だめです。UIのSearchBarのdelegateが関数を追加した WebSearchViewController であることを示す必要があります。IBに戻って SearchBarをControlを押しながらクリック、そのまま File'sOwner(WebSearchViewController)にドラッグしてOutletsのDelegateと繋げます。
これでデバッガコンソールでビルド実行してみてください。検索ボタンを押すとキーワードがコンソールに表示されます。ちなみに、
[searchBar resignFirstResponder];
はキーボードを閉じる処理をしています。
さて、次にこのキーワードをもちいてウエブ検索APIからデータを取得しましょう。次のような関数を作ります。
static NSString *webSearchAPIUrl = @"http://search.yahooapis.jp/WebSearchService/V1/webSearch?appid=p3O1lFOxg66up7VWRDmVkEJnF6baGNJj38rT6gUY3DzyF.kX4_PepRsfnk1AYss1J.YG&results=20&query="; - (void)searchByYahooAPI:(NSString *)query{ NSXMLParser *parser = [[NSXMLParser alloc] initWithContentsOfURL:[NSURL URLWithString:[NSString stringWithFormat:@"%@%@",webSearchAPIUrl,query]]]; [parser setDelegate:self]; [parser parse]; [parser release]; }
NSXMLDocumentというXMLのDOM系クラスがAppleCoreLibraryにありますが、残念ながらiPhoneでは使えません(シミュレータではエラー無く使えてしまうので注意)。iPhoneでXMLを処理するには現状SAX系のNSXMLParserを利用します。NSXMLParserのinitWithContentsOfURLを利用すると、外部リソース(URL)からXMLを簡単に取得してくれます。NSXMLParserはXML解析用の関数をdelegateで定義します。このクラス(WebSearchViewController)に書きますのでselfに指定しています。SAX関数を用意する前に、結果を保存する変数を用意しておきます
WebSearchViewController.hを以下のように変更します
@interface WebSearchViewController : UIViewController { IBOutlet UITableView *rTable; @private NSMutableString *tempXMLString; //XMLの要素文字列を保存する変数 NSMutableArray *webResult; //検索結果全体を保存する配列(要素はDictionary) NSMutableDictionary *siteInfo; //検索結果1件分の情報(各要素)を保持する }
Table Viewの変数もついでにここで書いておきました。さらにもう少し準備しておきます。
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar { if(!webResult) { webResult = [[NSMutableArray alloc] init]; } else { [webResult removeAllObjects]; } [searchBar resignFirstResponder]; [self searchByYahooAPI:searchBar.text]; }
- (void)dealloc { [webResult release]; [super dealloc]; }
初回検索時にNSMutableArrayをメモリ上に確保します。次の検索の際は結果をクリアします。定義した検索を行う関数(searchByYahooAPIにキーワードを渡しています).
さて、SAX関数を定義します
- (void)parserDidStartDocument:(NSXMLParser *)parser { NSLog(@"start"); } - (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict { tempXMLString = [NSMutableString string]; if ([elementName isEqualToString:@"Result"]) { siteInfo = [[NSMutableDictionary alloc] init]; } } - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName { if ([elementName isEqualToString:@"Title"]) { [siteInfo setObject:tempXMLString forKey:elementName]; } if ([elementName isEqualToString:@"ClickUrl"]) { [siteInfo setObject:tempXMLString forKey:elementName]; } if ([elementName isEqualToString:@"Result"]) { [webResult addObject:siteInfo]; [siteInfo release]; siteInfo = [[NSMutableDictionary alloc] init]; } } - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string { [tempXMLString appendString:string]; } - (void)parserDidEndDocument:(NSXMLParser *)parser { [siteInfo release]; NSLog(@"end"); NSLog(@"%@", webResult); }
NSXMLParserの5つのdelegateの関数を定義しています
- parserDidStartDocument
- didStartElement
- didEndElement
- foundCharacters
- parserDidEndDocument
簡単に説明すると、XMLのResultタグ毎にsiteInfo変数に属性(今回はTitle,ClickUrlのみ)を保存しています。Resultの閉じタグで、1件分の検索結果が出来上がりますので、webResultの変数に保存しています。この部分の各自で色々な箇所でNSLogを用いて変数を確認すると理解が深まりますので試してみてください。
デバックコンソールで起動してみましょう。検索キーワードをいれて検索ボタンをおすとコンソールに XML解析が終わった時点でつけたデバック関数
NSLog(@"%@", webResult);
の出力が確認できると思います。日本語の部分が内部エンコードで出力されるので読めませんが問題ありません
つぎは最後にこの出力結果をTable Viewに表示します。 Table Viewはすこし表示のさせ方がユニークで最低2つの関数を定義します
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
上が、このテーブルには何件データが含まれているかを返却する関数です.下が、各要素(何番目の要素かは引数で与えられる)では何を表示するか (UITableViewCellを返却する).
これらを定義したクラスをTableViewのdataSorceで指定します(delegateみたいな感じに).
また、良く利用されるTable Viewのdelegateは
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
です。これは何番目がクリック(タップ)されたときにどういう動作をするかを定義する関数です.
それぞれを以下のように定義します。
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return [webResult count]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { UITableViewCell *cell; cell = [[UITableViewCell alloc] init]; cell.text = [[webResult objectAtIndex: indexPath.row] objectForKey: @"Title"]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:[[webResult objectAtIndex: indexPath.row] objectForKey: @"ClickUrl"]]]; }
今回はクリック時にはSafariが起動してURLを開くようにしました。テーブルの再描画は reloadDataをtableに対しておこないますのでXMLの解析終了時にそれを行うようにします。
- (void)parserDidEndDocument:(NSXMLParser *)parser { [siteInfo release]; NSLog(@"end"); NSLog(@"%@", webResult); [rTable reloadData]; }
あともう一つ作業がひつようです、SearchBarの時と同じようにIBに戻って以下の定義を行います
- IB の TableViewとは WebSearchViewController の rTableですよ
- IB の TableViewの dataSourceと delegateは WebSearchViewController
です。上はFile'sOwnerからTableViewへ, 下はTableViewからFile'sOwnerに向かってドラックします。
以上で完成です。ビルドして実行してみましょう
検索アプリケーションが完成しました。結果をクリックする度にSafariが起動してプログラムが終了してしまうなど正直言ってこのままでは使い物になりませんね。勉強用プログラムと割り切ってもっと使いやすいもの後編でしていきましょう。また、今回は何でもWebSearchViewControllerに追記してしまいあまり良いプログラム設計とはいえません、この辺も後編で改善していきましょう。
iPhoneプログラミング(後編)へ続きます。
iPhoneプログラミング(前編)
iPhoneネイティブアプリ
iPhone (iPodTouch) が新しいモバイルデバイスとして注目されています。iPhoneにはSafariブラウザが搭載されているので、Safari上で動作するウエブアプリ開発は通常の開発の延長で可能です。(Apple Developer Connection - Web Apps Dev Center)
iPhoneではネイティブアプリとしてSafariを経由せずに直接iPhoneOS上で動作するアプリケーションも開発することが可能です。ネイティブアプリの開発にはiPhoneSDKを開発環境としてもちいます。現状, intel系macのみで開発が可能です。iPhoneSDKの入手および利用にはAppleのiPhone Dev Centerの利用登録が必要です。
【AppleとのNDA】
08年10月まではiPhone開発(SDK)に関する一切の情報はNDAに縛られ開発者が自由にその開発方法を公開するのは禁止されていました(iPhoneSDKの入手時にその規約に同意する必要があった)が、現時点では公開済みの機能に限って(iPhoneSDKで公開前にSDKにその機能を含めて提供する場合があるため)、その制限が緩和されました。それに伴い、たくさんのブログ等で開発に関する情報が掲載されてきています。
iPhoneプログラミング
iPhoneはObjectiveCと呼ばれる開発言語で開発します。ObjectiveCはC言語にオブジェクト指向を適用したような(C++のものとは文法が異なる)言語です。mac用アプリケーションの開発も同様にこの言語が用いられています。iPhoneSDKをインストールするとObjectiveCの開発環境にあわせて、MacOS(iPhoneOS)で動作するたくさんのライブラリ(機能系,UI系)がインストールされます。これらを組み合わせてプログラムを行います。
iPhoneSDKはmacの統合開発環境Xcodeに組み込む形で利用しますXcodeはiPhoneだけでなく通常のmacアプリを開発する際にも利用されます(ダウンロードしたiPhoneSDKにXcodeも含まれれるので別途入手する必要がありません)
mac(iPhone)のアプリ開発はある種特徴的な部分が多く慣れるまでは理解が難しい部分が多いですが、開発に関する膨大な技術資料がiPhone Dev Centerから閲覧できます。iPhoneSDKを本格的に使いこなす場合はどれも有益な内容だと思います。
また。Xcodeの【ヘルプ】メニューから 【Xcodeワークスペースガイド】からライブラリのリファレンスが参照できます。たくさんのライブラリを組み合わせてiPhoneのアプリケーションは組み立てます。どのようなライブラリがあるか、各ライブラリの機能は何かを調べる際に頻繁に参照するガイドになります。
iPhoneSDKにはiPhoneの動作をシミュレーションするiPhoneシミュレータが含まれます。これを用いるとプログラムの動作確認を実機を使用せずに確認することが可能です(当然のiPhone実機でのみ確認できる機能もあります. 加速度センサ/GPS/カメラなど)
iPhoneアプリを実機で確認、および配布するためには別途Appleの有償のアカウント(Apple Developer Program)が必要になります。この部分の説明は割愛します。
iPhoneでHelloWorld
iPhoneのプログラムの詳細な説明は非常に大変なのでチュートリアル風に説明します.各作業が何を意味しているかの説明は都度リファレンスなどを調べて理解してください。
Xcodeを起動して【ファイル】から【新規プロジェクト】を選択します。プロジェクトテンプレートの選択になりますのでView-Based Applicationを選択します。
次にプロジェクト名を入れます。HelloWorldとします。指定した場所にフォルダが作成されます。1つのプロジェクトで必要なコードはこのフォルダにまとめられます。これでプロジェクトが作成されました。下記の画面がXcodeの開発画面です.
フォルダ内には以下のファイルなどが作成されます
HelloWorld |-- Classes | |-- HelloWorldAppDelegate.h | |-- HelloWorldAppDelegate.m | |-- HelloWorldViewController.h | `-- HelloWorldViewController.m |-- HelloWorld.xcodeproj |-- HelloWorldViewController.xib |-- HelloWorld_Prefix.pch |-- Info.plist |-- MainWindow.xib |-- build | `-- HelloWorld.build `-- main.m
HelloWorld.xcodeprojがXcodeのプロジェクトファイルです。これを起動すると次回このプロジェクトのXcode開発画面が開きます。まず、この状態でXcode上部の【ビルドして実行】を選択します。するとプロジェクトがビルドされ、iPhoneシミュレータが起動します
Viewが見えるだけで何も定義していない画面が表示されます。まずは、細かい説明はせずにHelloWorldまで進みます。
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
次にXcode左のリソースツリーの
HelloWorldViewController.xibをダブルクリックします xibはUserInterfaceを定義するファイルでこれを編集するInterface Builderが起動します。
Interface BuilderのLibraryから
- Label を設置してHelloWorldと入力してください
- Round Rect Button を設置してしてpush!と入力してください
設置はViewの画面にドラックすれば設置されます。文字の入力は設置しされたコンポーネントをダブルクリックするかAttributeパネルのtextやtitleに入力します。
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
次にXcodeに戻って編集します. HelloWorldViewController.hを編集します
#import <UIKit/UIKit.h> @interface HelloWorldViewController : UIViewController { IBOutlet UILabel *label; } -(IBAction)pushButton:(id)sender; @end
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
Xcodeでファイルを編集保存したあと、Interface Builder(HelloWorldViewController.xib)に戻りFile'sOwnerを選択するとXcodeでIBOutletやIBActionで定義したものが表示されます。
次に定義したコンポーネントをこのIBOutletとIBActionにリンクさせます。
- File'sOwnerのアイコンをControlを押しながらクリックしてViewのLabelへドラッグします(青線で結びます). 黒窓がでるのでlabelを選択します.
- ViewのボタンをControlを押しながらクリックして今度は逆にFile'sOwnerへドラックします。黒窓がでるのでpushButtonを選択します。
最終的にFile'sOwnerをControlを押しながらクリックするとリンク内容が表示されます。下記のようになればOKです
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
Xcodeに再び戻ります。次に編集するファイルはHelloWorldViewController.mです
-(IBAction)pushButton:(id)sender{ NSLog(@"pushed button"); label.text = @"iPhone Program"; }
ボタンがクリックされた場合の動作を定義します。labelのtextの内容を書き換えます。またデバッガにクリックした情報を落とします。これでプログラムの完成です。デバックコンソール付きで実行するにはXcodeのメニューの【実行】から【コンソール】を選択します。コンソール画面が表示されますので、そこの上部の【ビルドして実行】を実行します。
クリックするとラベルの内容が変わります。またコンソールにNSLogで指定したメッセージが表示されると思います。
少し詳しく解説
iPhoneのプログラムはmain.mからスタートします
#import <UIKit/UIKit.h> int main(int argc, char *argv[]) { NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init]; int retVal = UIApplicationMain(argc, argv, nil, nil); [pool release]; return retVal; }
このmainと同じくinfo.plistが読み込まれます。ここに定義されている
Main nib file base name = MainWindow
という定義によりmainのUIApplicationMain関数がMainWindow.xibを読み込んで起動します。MainWindow.xibは前のサンプルでは利用しませんでしたがInterfaceBuilderで同じく起動できます。iPhoneを起動するとUIApplicationのインスタンスが生成されます。MainWindow.xibはこのUIApplicationのインスタンスになります。(File'sOwner参照)
ObjectiveCのプログラムではインスタンスの機能はdelegateという機能で委譲(別インスタンスに定義)させます。UIApplicationのdelegate関数はリファレンスライブラリで (UIApplicationDelegate Protocol ) を参照します。ここに定義されているメソッド類はUIApplicationの動作を定義するもので、UIApplicationDelegateをインスタンス化したあとにUIApplicationのdelegateに接続することでその動作が行われます。(この辺はすこしObjectiveCならではの概念ですが、さまざまなクラスでdelegateを使用する機会があるので別途リファレンスを参照して理解してください)
MainWindow.xibのFile'sOwner(すなわちUIApplication)のdelegateがHellowWorld App Delegateに接続しているのが分かります。
さらに、HellowWorld App Delegateを選択して情報をみると下記のように表示されます
右のClassの表示でこれがHelloWorldViewControllerクラスのインスタンスであることが分かります HelloWorldViewControllerはXcode上に定義されいます。HelloWorldViewController.hの定義で
@class HelloWorldViewController; @interface HelloWorldAppDelegate : NSObject <UIApplicationDelegate> { UIWindow *window; HelloWorldViewController *viewController; } @property (nonatomic, retain) IBOutlet UIWindow *window; @property (nonatomic, retain) IBOutlet HelloWorldViewController *viewController;
とあります。windowやviewControllerはIBOutletで定義されているのでInterfaceBuilderで表示され、それぞれwindowとHelloWorld View Controllerにリンクさせています. HelloWorldViewController.m で
- (void)applicationDidFinishLaunching:(UIApplication *)application { // Override point for customization after app launch [window addSubview:viewController.view]; [window makeKeyAndVisible]; }
の関数が定義されています. applicationDidFinishLaunchingこれがUIApplicationのdelegateの一つでアプリケーションが起動時に呼ばれる関数です。イメージ的にはwindowの上にviewControllerを乗せていますviewControllerはMainWindow.xibでHellowWorld View Controllr の名前で定義されており HelloWorld App DelegateのviewControllrとリンクさせています。HellowWorld View Controllr は XcodeのHelloWorldViewControllerのクラスのインスタンス実体であり、またxibは別にHelloWorldViewController.xibファイルで定義させています。
HelloWorldViewController.xibのViewの上に先ほどのHelloWorldプログラムを構築しました。
UIApplication -> window -> ViewController -> View -> HelloWorld (Label,Button)
という階層でこのHelloWorldアプリが動いていたことになります。
iPhoneプログラミング(中編)も参照下さい。
flash - ActionScript (後編)
前編ではyoutube APIを用いて動画キーワード検索を行い、検索結果部分にサムネイルで動画検索結果が表示できるところまで作りました。今回は、仕上げととして、サムネイルをダブルクリックすると右側の再生部分で動画再生させるところまでを作ります。完成図は以下の形です。
flexで動画を流すのは flv 形式のファイルをVideoDisplayというコンポーネントで再生する形を採用します. youtubeの各動画のflv形式をファイルの取得はAPIでは応答されません。そのため、一度youtubeの再生ページのHTMLを別途取得して
HTMLを解析しflv動画ファイルのダウンロードURLを取得することにします。youtubeの動画再生ページのURLは
http://www.youtube.com/watch?v=n0fhz1SGnSE
であり、下記の画面です。この画面のplayerもflvをyoutubeサーバからflvをダウンロードしてplayerで再生しているflashアプリケーションです。よってこのページにflvの在処の手がかりがあります。
この部分はGoogle社が公式にはアナウンスしていないので詳細の説明は省きますが、実際には
http://www.youtube.com/get_video?video_id=n0fhz1SGnSE&t=OEgsToPDskKEImaKT-gE2L7iHGQnGnGt
というURLでflvが取得しできます。このURLのパラメータvideo_idおとびtはHTMLのソース上に記載されています。下記はHTMLの該当の部分
var swfArgs = {"usef": 0, "BASE_YT_URL": "http://www.youtube.com/", "vq": null, "sourceid": "yw", "video_id": "n0fhz1SGnSE", "l": 15, "sk": "rkW--sJoRE6d6nxtGT6xGz3jFddjU0CAC", "fmt_map": "6/720000/7/0/0,34/0/9/0/115", "t": "OEgsToPDskKEImaKT-gE2L7iHGQnGnGt", "hl": "ja", "plid": "AARdWT8FsjBhDANPAAACgAAoAAA", "sdetail": "rv%3An0fhz1SGnSE"};
これを取得しflvダウンロードURLをXMLで応答するproxyを作成し、こちらのplayerから取得する方式をとります。例えば単純なものだと以下のようなCGIになります。
bash-3.2$ cat prog10/youtube_proxy.php <?php $curl = curl_init(); curl_setopt ($curl, CURLOPT_URL, "http://www.youtube.com/watch?v=${_GET['video_id']}"); curl_setopt ($curl, CURLOPT_RETURNTRANSFER, 1); $html = curl_exec ($curl); preg_match('|"video_id": "(.*?)"|', $html, $v); preg_match('|"t": "(.*?)"|', $html, $t); print "<url>"; print "http://jp.youtube.com/get_video?video_id=${v[1]}&t=${t[1]}"; print "</url>"; ?>
これにvideo_id(APIから取得可能)を引数にアクセスすると
<url> http://jp.youtube.com/get_video?video_id=n0fhz1SGnSE&t=OEgsToPDskKEImaKT-gE2L7iHGQnGnGt </url>
とflvの所在のURL結果が応答されます.
さて、FlexBuilderに戻ります。検索結果のサムネイルをダブルクリックした場合に動画が再生されるようにしたいので、 まず、サムネイルのダブルクリックを検知し、関数が呼ばれる仕組みを実装しましょう.
TileListのプロパティに
doubleClickEnabled="true" doubleClick="playmovie()"
を加えます。 doubleClickEnabledを trueにするのを忘れないようにしてください。それで playmovie関数を定義すればそれが呼ばれます。今回はもう少し突っ込んで進みましょう。最終的に やりたいことはタブルクリックしたら
- ダブルクリックしたサムネイルの video_idをしらべて
- そのvideo_idを先ほどのflvが取得できるURLを獲得できる自作のapiに投げて
- その応答のURLを動画再生のsourceにセットする
です。なので、1.のダブルクリックしたサムネイルのvideo_idまで取得できるように実装しましょう.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ ....省略.... public function playmovie():void { var tmp:Array = movielist.getItemAt(tilelist.selectedIndex).id.split('/'); var video_id:String = tmp[tmp.length-1]; Alert.show(video_id); } ]]> </mx:Script> ....省略.... <mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%" dataProvider="{movielist}" itemRenderer="movieitem" doubleClickEnabled="true" doubleClick="playmovie()" id="tilelist" /> </mx:HDividedBox> ....省略.... </mx:Application>
playmovieの関数で, tilelist.selectedIndex が何番目の listを今選択しているかが取得できます. (TileListのidをtilelistにしました.) movielist.getItemAt(tilelist.selectedIndex).id でその選択している entryの idタグ部分が 取得できます。このidは
<id>http://gdata.youtube.com/feeds/api/videos/_EEx3VhloNg</id>
となっている部分なので、実際のvideo_idは最後の/の後ろの部分なので、 / でsplitして、最後の部分を取得しています方法をとりました。これで実行してサムネイルをダブルクリックしてみてください. video_idが表示されるはずです.
次にvideo_idが取得できたら、これを自作のAPIに投げて flvのURLが取得できるところまで実装しましょう。APIへのリクエスト&受信はまったくyoutube API の部分と同じ感じです。一気に実装します.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ import mx.rpc.events.ResultEvent; import mx.controls.Alert; import mx.collections.ArrayCollection; [Bindable] public var movielist:ArrayCollection = new ArrayCollection; public function send_request():void { var uv:URLVariables = new URLVariables; uv.vq = keyword.text; youtubeapi.request = uv; youtubeapi.send(); } public function youtubeapi_result(event:ResultEvent):void { movielist.removeAll(); for(var i:int=0;i<event.result.feed.entry.length;i++) { movielist.addItem(event.result.feed.entry[i]); } } public function playmovie():void { var tmp:Array = movielist.getItemAt(tilelist.selectedIndex).id.split('/'); var video_id:String = tmp[tmp.length-1]; var uv:URLVariables = new URLVariables; uv.video_id = video_id; flvurlapi.request = uv; flvurlapi.send(); } private function flvurlapi_result(event:ResultEvent):void { Alert.show(event.result.url); } ]]> </mx:Script> <mx:ApplicationControlBar width="100%"> <mx:TextInput id="keyword" /> <mx:Button label="youtube search" click="send_request()"/> </mx:ApplicationControlBar> <mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%" dataProvider="{movielist}" itemRenderer="movieitem" doubleClickEnabled="true" doubleClick="playmovie()" id="tilelist" /> </mx:HDividedBox> <mx:HTTPService id="youtubeapi" url="http://gdata.youtube.com/feeds/api/videos" method="GET" result="youtubeapi_result(event)" /> <mx:HTTPService id="flvurlapi" url="http://localhost:8888/prog10/youtube_proxy.php" method="GET" result="flvurlapi_result(event)" /> </mx:Application>
youtubeAPIにアクセスしたときと同じです。もう一つ HTTPServiceをつくって、ダブルクリックで呼ばれる関数で send して 応答を受信する。event.result.url で flvファイルのダウンロードURLが取得できます.
次にplayerを作ります。playerは VideoDisplayという 非常に便利なコンポーネントがありますのでこれを使います. メインの画面にこの VideoDisplayを 設置して id="player" とつけて player.source = で先ほどのflv のURLをセットすればそれで 再生できてしまいます。それだと応用性に欠けるのでplayerを独立したモジュールで作ってみましょう。簡単に再生したい方は上の方法をトライしてみてください.
flvのplayerは別につくるアプリでも使う可能性がある. とか、 再生とか停止とかタイムとかそういうデザインをメインのmxmlに加えると混乱するなどという場合に mxmlファイルを別に定義してそれを読み込む形ができます。ちょうど検索結果タイルで使用したの itemRenderer と似ているような感じです。早速playerを独立したmxmlファイルで定義してみましょう.
まず、左上のflexナビゲータのsrcのフォルダを右クリックして「新規」-「フォルダー」で module という名前のフォルダーを作ってください. 実際にはこの名前は何でもいいです.次に, 先ほどの movieitem.mxmlを作っときと同様にメニューから 「ファイル」-「新規」-「MXMLコンポーネント」を選びます.
ファイル名: player ベース: VBox 幅: 250 高さ:250
とします.作成先が module のフォルダの下になるように設定してください.最終的には下のようになればokです
さて、ではこのplayer.mxmlに 動画プレイヤーの各機能を作っていきます。
一般的な動画再生サイトのplayerはグラフィカルな停止再生ボタンや、音量調節、再生ヘッド位置とか 色々表示されていますが、今回はUI的なことに凝ると本題から外れるので最低限の以下の機能&情報を 表示することにしましょう.
- 動画再生画面
- 中断/再生ボタン
- 読み込み済みbyte数 と Total byte数
- 再生の経過時間 と Total再生時間
です。 とりあえず上の情報を並べると、例えば
というレイアウトでmxmlファイルは下記のようになります。
<?xml version="1.0" encoding="utf-8"?> <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="250" height="250"> <mx:Label text="my youtube player"/> <mx:VideoDisplay id="vd" width="100%" height="100%" /> <mx:HBox> <mx:Button label="play" /> <mx:Button label="stop" /> </mx:HBox> <mx:HBox> <mx:Label text="Loading Info:" /> <mx:Label id="byteloaded" /> <mx:Label text="/" /> <mx:Label id="bytetotal" /> </mx:HBox> <mx:HBox> <mx:Label text="Playing Info:" /> <mx:Label id="playtime" /> <mx:Label text="/" /> <mx:Label id="totaltime" /> </mx:HBox> </mx:VBox>
という風になります。idは必要な箇所にすでに割り当てました.動画プレイヤー風になるように デザインビューなどをみてみて調節ください.さて、このプレイヤーを本体 (youtube_player.mxml) に設置するにはどうすればいいでしょう.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:module="module.*" layout="vertical">
まず、1行目に
mlns:module="module.*"
という記述でmodule(フォルダ)以下のコンポーネントを読み込みますという宣言をします
するとmxml上で
<mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%" dataProvider="{movielist}" itemRenderer="movieitem" doubleClickEnabled="true" doubleClick="playmovie()" id="tilelist" /> <module:player id="flvplayer" /> </mx:HDividedBox>
という風に配置することができます(TileListと並列に並べてみました)
さて、これで実行してみてください。基本のロジックは変えていませんが、一番最初に HDividedBox を設置してから右側に何も無かった部分にplayer.mxmlで定義したモジュールが加わったと思います。
もう少しです。まずは、ダブルクリックで再生できるようにしてしまいましょう. playerで定義した VideoDisplayのsourceに先ほどAPIで取得した flv のURLをセットすれば再生されます.
これをどのように実装するかは下記になります. Bindable で 変数 flvurl を定義しそれを VideoDisplayの sourceにセットする. こうすると youtube_player.mxml で先ほど定義した flvplayer から flvplayer.flvurl でアクセスできます.
player.mxml
<?xml version="1.0" encoding="utf-8"?> <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="250" height="250"> <mx:Script> <![CDATA[ [Bindable] public var flvurl:String = new String; ]]> </mx:Script> <mx:Label text="my youtube player" /> <mx:VideoDisplay id="vd" width="100%" height="100%" source="{flvurl}"/> <mx:HBox> <mx:Button label="play" /> <mx:Button label="stop" /> </mx:HBox> <mx:HBox> <mx:Label text="Loading Info:" /> <mx:Label id="byteloaded" /> <mx:Label text="/" /> <mx:Label id="bytetotal" /> </mx:HBox> <mx:HBox> <mx:Label text="Playing Info:" /> <mx:Label id="playtime" /> <mx:Label text="/" /> <mx:Label id="totaltime" /> </mx:HBox> </mx:VBox>
そのあとに本体のyoutube_player.mxmlのyoutube_proxyのAPI受信関数に
private function flvurlapi_result(event:ResultEvent):void { flvplayer.flvurl = event.result.url; }
とします。flvurlがpublic変数なので外部からアクセスでき、またBindableなので、VideoDisplayのsourceプロパティと変数が同期されます。
実行してみてください。 検索してサムネイルをダブルクリックすると動画がplayerで再生されます.
最後のplayerの装飾はplayer.mxml内部で完結します
<?xml version="1.0" encoding="utf-8"?> <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="250" height="250" creationComplete="init()"> <mx:Script> <![CDATA[ import mx.events.VideoEvent; [Bindable] public var flvurl:String = new String; private function init():void { vd.addEventListener(ProgressEvent.PROGRESS, function():void { byteloaded.text = vd.bytesLoaded.toString(); bytetotal.text = vd.bytesTotal.toString(); playtime.text = vd.playheadTime.toString(); totaltime.text = vd.totalTime.toString(); }); vd.addEventListener(VideoEvent.PLAYHEAD_UPDATE, function():void { byteloaded.text = vd.bytesLoaded.toString(); bytetotal.text = vd.bytesTotal.toString(); playtime.text = vd.playheadTime.toString(); totaltime.text = vd.totalTime.toString(); }); } ]]> </mx:Script> <mx:Label text="my youtube player" /> <mx:VideoDisplay id="vd" width="100%" height="100%" source="{flvurl}"/> <mx:HBox> <mx:Button label="play" click="vd.play()"/> <mx:Button label="stop" click="vd.pause()"/> </mx:HBox> <mx:HBox> <mx:Label text="Loading Info:" /> <mx:Label id="byteloaded" /> <mx:Label text="/" /> <mx:Label id="bytetotal" /> </mx:HBox> <mx:HBox> <mx:Label text="Playing Info:" /> <mx:Label id="playtime" /> <mx:Label text="/" /> <mx:Label id="totaltime" /> </mx:HBox> </mx:VBox>
playとstop(pause)のボタンへの割当はソースの通りButtonのclickに割り当てます。動画読み込み中、再生中に 各種データ(読み込みbyte, 再生時間の更新をするために, VideoDisplayの addEventListener の
- ProgressEvent.PROGRESS = FLV ファイルが完全にダウンロードされるまで継続的に送出されます。
- VideoEvent.PLAYHEAD_= UPDATE ビデオの再生中に 0.25 秒ごとに送出されます
というイベントでUpdate通知させます. 各種情報は VideoDisplayのリファレンスから抜き出せばもっとリッチなplayerがつくれます.
さて、長々説明してきた Flex builder で作る youtubeが検索できるplayer付きアプリ もこれで完成しました。かなり応用ができる範囲まで説明したと思いますので、それぞれ楽しいアプリケーションが作れることを期待します。
今回のプログラム完成版は
http://trac.assembla.com/cX0noKTPir3A7Oab7jnrAJ/browser/prog10
にありますので、必要に応じて参照してください。
flash - ActionScript (前編)
今回はFlashアプリの作り方を解説します。flashを利用するとリッチで動的なコンテンツをユーザに提供可能なので、さまざまなマルチメディアコンテンツに利用されています。華やかな企業のホームページもflashで作られていますが、今回はウエブアプリケーションという面に重点を置いて解説したいと思います。flashは一般的なPCブラウザに組み込まれたadobeのFlashPlayer上で動作するアプリケーションで、Adobe Flashという製品を用いて開発することが一般的ですが、ActionScriptをFlexSDKを用いてコンパイルしてflashアプリケーションを開発することも可能です。またFlexSDKを開発する際の統合開発環境としてFlexBuilderというソフトもあります。無料体験版およびアカデミック無料版があります(通常8万以上する製品が無料で入手可能なので学生さんはお得です)。現在はAdobeAIRという形でウエブブラウザへの組み込みなしで(ディスクトップアプリとして)flashアプリを動作させることも可能になりました。
flashプログラミング
flashプログラミングはActionScriptをベースにFlexライブラリをimportしてコーディングするスタイルがありますその方法を用いてHelloWorldプログラムを作った場合の例は
bash-3.2$ cat prog10/helloworld/HelloWorld1.as package { import flash.display.Sprite; import flash.text.TextField; public class HelloWorld1 extends Sprite { public function HelloWorld1(){ var tf:TextField = new TextField; tf.text = "Hello World"; addChild(tf); } } }
bash-3.2$ /Applications/Adobe\ Flex\ Builder\ 3/sdks/3.0.0/bin/mxmlc HelloWorld1.as 設定ファイル "/Applications/Adobe Flex Builder 3/sdks/3.0.0/frameworks/flex-config.xml" をロードしています /Applications/MAMP/htdocs/prog10/helloworld/HelloWorld1.swf (621 bytes)
とHelloWorld1.swf が作成されます(mxmlcのパスは上記例はFlexBuilderをデフォルトでインストールした場所)
もう一つの方法としてMXMLと呼ばれるファイルを基本としてActionScriptを利用してプログラムを記述します。
MXMLはXML言語で記述され、アプリケーションのユーザインタフェースを配置することができます。
bash-3.2$ cat prog10/helloworld/HelloWorld2.mxml <?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init();"> <mx:Script> <![CDATA[ private function init():void{ tf.text = "Hello World"; } ]]> </mx:Script> <mx:Text id="tf" /> </mx:Application>
MXMLを記述したら同じくコンパイルすることによって swf (flashファイル)を作成します
bash-3.2$ /Applications/Adobe\ Flex\ Builder\ 3/sdks/3.0.0/bin/mxmlc HelloWorld2.mxml 設定ファイル "/Applications/Adobe Flex Builder 3/sdks/3.0.0/frameworks/flex-config.xml" をロードしています /Applications/MAMP/htdocs/prog10/helloworld/HelloWorld2.swf (153105 bytes)
実行は作成されたswfを実行すればSA(スタンドアロン)版のPlaryerで起動します。ブラウザで起動しても実行されます。
簡単なプログラムサンプルを含めて下記のページにチュートリアルがありますので参考にしてください。
https://www.adobe.com/jp/devnet/flex/quickstart/coding_with_mxml_and_actionscript/
また、Adobeが提供するflex各種ドキュメントは以下にあります
FlexBuilderで作るflashマルチメディアアプリケーション
今回は、flashの利点を生かし、マルチメディアを意識した動画検索アプリケーションを作成してみます。動画はyoutubeのAPIを利用して、動画検索を行います。また、flashはflv形式の動画を簡単に再生することが可能なので、youtubeからflvファイルをダウンロードし直接再生機能も持たせてみます。完成後の画面をまず見せます。
メイン上部に検索窓、メイン中央に検索結果をタイル上にサムネイル表示します。右側に再生画面を表示します。サムネイルをダブルクリックすることで動画が再生される仕様とします。
flexBuilderを起動すると以下のような画面が表示されます。
これから作成するアプリのプロジェクトを作成します。メニューから[ファイル] - [新規] - [MXMLアプリケーション] を選択します
プロジェクト名を入力します。今回は youtube_player としました。
終了を押すと、元の画面にプロジェクトのひな形が作成されました。ここから開発をスタートします。左上のflexナビゲータには各種リソースがあります。 src/youtube_player.mxmlのソースに編集を加えていくことになります。右のメイン画面にはそのソースが開いた状態になっていると思います。
次にまずはデザインを配置してしまいましょう。今回使う主要なものは
- キーワードを入力する TextInput
- 検索ボタンの Button
- 検索結果を表示する TileList
- 動画を見せる player
基本的にこの4つがあれば、デザインはどんなものでも良いです。メインパネルの「ソース」と「デザイン」の切り替えで「デザイン」を押して作ります。(もちろんソースに直に書いてもokです). 私が行った方法を例にあげます。
Application(全体) の レイアウトを vertical にする.
ApplicationControlBar を配置し width:100% にする.
その中に TextInput と Button を配置する.
Button のラベルを youtube search にする
次にApplicationControlBarの下部にHDividedBoxを配置する
width:100% height:100%にする
その中に TileList を width:100% height:100%で配置する
この時点で youtube_player.mxmlのソースは自動的に編集され
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:ApplicationControlBar width="100%"> <mx:TextInput/> <mx:Button label="youtube search"/> </mx:ApplicationControlBar> <mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%"></mx:TileList> </mx:HDividedBox> </mx:Application>
となっているはずです。
では、ちょっとずつ勉強しながら作りましょう.まず TextInputにキーワードを入力して Buttonをクリックしたら Alertがあがるようにしましょう。そのためには、TextInput に id をつけて Button のクリック動作に関数を割り当てましょう.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ import mx.controls.Alert; public function send_request():void { Alert.show(keyword.text); } ]]> </mx:Script> <mx:ApplicationControlBar width="100%"> <mx:TextInput id="keyword" /> <mx:Button label="youtube search" click="send_request()"/> </mx:ApplicationControlBar> <mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%"></mx:TileList> </mx:HDividedBox> </mx:Application>
なにをやったかというと, TextInput に keywordというidを与え、Buttonの click イベントに send_request の関数を与えました。 mx:Script のタグを新たに設け、その中に send_requestの関数を処理を記入しました。今回は keywordの中身の文字列のAlert を出すのでその処理を記入しました。(Alertを出すには mx.controls.Alert を import する必要があるのでそれも加えてあります)
ここで実行を一度してみましょう。flex builder上部の緑色の丸いプレイアイコンをクリックするか 実行メニューを選ぶとswfをコンパイルしてブラウザが勝手に起動されると思います。 そこに先ほどデザインしたものが表示され、キーワード窓に適当に入力し、ボタンをクリックしたら Alertダイアログが表示されその中身はキーワード窓で入力したものが表示されていると思います。
さて、次に youtube APIを利用して動画リストを取得するのですが、 youtube APIの仕様を確認しておきましょう. APIにはさまざまなリクエスト方法がありますが、今回使用するのは単純にキーワード検索ですので
http://gdata.youtube.com/feeds/api/videos?vq=[キーワード]
です。実際にブラウザでアクセスしてみるとXMLが取得できると思います.flashで外部ドメインにアクセスする場合、外部APIのホスト直下にcrossdomain.xmlが設置され、かつ許可がされている場合に限りアクセス可能です
http://gdata.youtube.com/crossdomain.xml
<?xml version="1.0"?> <!-- http://gdata.youtube.com/crossdomain.xml --> <!DOCTYPE cross-domain-policy SYSTEM "http://www.macromedia.com/xml/dtds/cross-domain-policy.dtd"> <cross-domain-policy> <allow-access-from domain="*"/> <allow-http-request-headers-from domain="*" headers="*"/> </cross-domain-policy>
と書かれているので任意のドメインよりflashにてアクセス可能となっています。flexにてapiにアクセスする一般的な方法は HTTPServiceを利用することです。先ほどの youtube_player.mxml に
<mx:HTTPService id="youtubeapi" url="http://gdata.youtube.com/feeds/api/videos" method="GET" result="youtubeapi_result(event)" />
をまず加えます. 次に先ほど Alertを出していた関数にsend_request にAPIリクエストを投げる処理を加え、APIの応答が戻ってきたときに呼ばれる(HTTPService関数のresultで指定した)youtubeapi_result関数を作れば、youtubeAPIに flexからリクエストを投げて応答を受け取ったことになります. ここまでで プログラムは以下になります
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ import mx.rpc.events.ResultEvent; import mx.controls.Alert; public function send_request():void { var uv:URLVariables = new URLVariables; uv.vq = keyword.text; youtubeapi.request = uv; youtubeapi.send(); } public function youtubeapi_result(event:ResultEvent):void { Alert.show("get youtube result"); } ]]> </mx:Script> <mx:ApplicationControlBar width="100%"> <mx:TextInput id="keyword" /> <mx:Button label="youtube search" click="send_request()"/> </mx:ApplicationControlBar> <mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%"></mx:TileList> </mx:HDividedBox> <mx:HTTPService id="youtubeapi" url="http://gdata.youtube.com/feeds/api/videos" method="GET" result="youtubeapi_result(event)" /> </mx:Application>
説明していない部分は, send_request の中の
var uv:URLVariables = new URLVariables; uv.vq = keyword.text; youtubeapi.request = uv; youtubeapi.send();
ですが、 URLVariables クラスを用いると一般的なAPIの引数の &key1=val1&key2=val2 値を 一つの変数で保持できます. 今回は vq=キーワードなので uv.vq = キーワード となります. 後はこの変数を HTTPServiceの request にセットして APIに投げます (send).
ここでもう一度実行してみてください。 検索ボタンをクリックしてしばらく待つと応答が帰ってくると思います.
さて、youtube からの応答がXML帰ってきました。が中身を知らなければ何もできません. ブラウザでアクセスしてみて応答をソース表示し、中身のXMLを一度確認してみましょう. 注意深く見ると以下の構造をしていることが分かります
<?xml version='1.0' encoding='UTF-8'?> <feed .....> しばらく、ヘッダー的なタグが続く <entry>1件目の動画情報<entry> <entry>2件目の動画情報<entry> <entry>3件目の動画情報<entry> <entry>....<entry> <entry>n件目の動画情報<entry> </feed>
entryの中身に、タイトル,サムネイル情報,youtubeURLなどの情報があることが分かると思います。
それでは、flexからこのXMLの各要素にアクセスしてみましょう。応答が帰ってきたときに 呼ばれるために作った関数 youtubeapi_resultの引数に event:ResultEvent があります。これの event.result要素がこのXMLになります。なので、例えば entryの5件目のtitle は
event.result.feed.entry[5].group.title
になります。それでは、ひとまず正しくXMLを取得できているか確認するために youtubeapi_result関数を
public function youtubeapi_result(event:ResultEvent):void { Alert.show(event.result.feed.entry.length); for(var i:int=0;i<event.result.feed.entry.length;i++) { Alert.show(event.result.feed.entry[i].group.title); } }
として動作確認してみてください。取得した動画の本数とタイトルが順にAlertに表示されると思います.
さて、次はこの結果をTileListにリスト表示させます List, TileList, HorizontalList などの各要素は単純に dataProviderを用いてリスト表示をするのが単純な使い方ですが (flexリファレンス) 検索結果画面とか商品一覧とかの画面ではリストの一つ一つがUIデザインをもつものであることが多いです。そのために、この List系のコンポーネントは itemRenderer というプロパティをもちます。これを使えば、個々のItemの設計を別に定義できます (flexリファレンス)
文字で書くと良くわからないので実際にやってみます。 まず、TileList のタグを
<mx:TileList width="100%" height="100%" dataProvider="{movielist}" itemRenderer="movieitem"
とします. これの意味は TileListの要素はmovielistの変数の値います. 個々のitemの表示には movieitemコンポーネント (movieitem.mxml) を使います. という意味です. movielist (ArrayCollection) も加えた形は下記になります (movielistは Bindable として登録する必要があります). youtubeapi_resultの処理をmovielistの初期化および entry リストの代入を行うように変更しました.
<?xml version="1.0" encoding="utf-8"?> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"> <mx:Script> <![CDATA[ import mx.rpc.events.ResultEvent; import mx.controls.Alert; import mx.collections.ArrayCollection; [Bindable] public var movielist:ArrayCollection = new ArrayCollection; public function send_request():void { var uv:URLVariables = new URLVariables; uv.vq = keyword.text; youtubeapi.request = uv; youtubeapi.send(); } public function youtubeapi_result(event:ResultEvent):void { movielist.removeAll(); for(var i:int=0;i<event.result.feed.entry.length;i++) { movielist.addItem(event.result.feed.entry[i]); } } ]]> </mx:Script> <mx:ApplicationControlBar width="100%"> <mx:TextInput id="keyword" /> <mx:Button label="youtube search" click="send_request()"/> </mx:ApplicationControlBar> <mx:HDividedBox width="100%" height="100%"> <mx:TileList width="100%" height="100%" dataProvider="{movielist}" itemRenderer="movieitem" /> </mx:HDividedBox> <mx:HTTPService id="youtubeapi" url="http://gdata.youtube.com/feeds/api/videos" method="GET" result="youtubeapi_result(event)" /> </mx:Application>
さて、これで実行するとエラーになるはずです. movieitem が未定義なのが原因です。 movieitem を作りましょう.
FlexBuilderのメニューから[ファイル] - [新規] - [MXMLコンポーネント] を選択して
ファイル名: movieitem ベース: VBox 幅: 150 高さ:150
で終了すれば movieitem.xml が左上の flexナビゲータに追加されます. この状態で実行すると先ほどのエラーは解消されます、がまだ何も表示されません が、マウスでTileListをなぞるとitemが選択されている状態になります。
movielist.xml の記述に入ります. メインの画面を movielist.mxmlに切り替えて Image および Text を配置します。デザインから行っても構いませんが、最終的なソースは
<?xml version="1.0" encoding="utf-8"?> <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" width="150" height="150" horizontalAlign="center"> <mx:Image height="97" width="130" source="{data.group.thumbnail.getItemAt(0).url}"/> <mx:Text text="{data.group.title}" width="100%" height="100%"/> </mx:VBox>
になります. Imageはイメージ画像をロードするコンポーネントです. ここで重要なのが, TileListのdataProviderの各要素がこのコンポーネントに渡されるのですが、その各要素は data の変数から 取得できるということです. なので youtube の entry を各要素として TileListのdataProviderに渡しているからこの中では entryのtitleは data.group.title で取得できます. 1つめのサムネイル画像は data.group.thumbnail.getItemAt(0).url で取得できます (data.group.thumbnail[0].urlとするとバインディングエラーがでるので注意).
これで実行してみましょう
これで、youtube検索結果がサムネイル付きで表示できるところまで完成しました。playerの部分は次回解説します。
携帯(モバイル)への対応
現在、アプリケーションを構築するにあたり、携帯でも動作可能とすることはたくさんのユーザに使ってもらえる機会を増やすだけでなく、PCと連動したアプリケーションを開発検討することができ、アプリケーションの利便性や機能性を大きく向上させることができる。アプリケーションを携帯に対応させる場合、CGIのサーバサイドではPC版と特に変わったことを意識する必要はない。携帯を意識する箇所はおもに入出力の箇所であり、制限(制約)がある。携帯に対応するためのアプリケーションにもと求められる対応箇所や携帯ならではの機能を以下に明記します.
出力するHTMLのファイルサイズに制限があります
HTMLのタグに関しても携帯のHTMLでは対応できないものがあります. またXHTMLに準拠して記述することが求められます。制限の仕様は端末やキャリア毎に異なります。シミュレータなどを利用しデザインを確認する必要があります。
徹底検証!携帯シミュレーター(ThinkIT)
JavaScript は利用できません
JavaScriptは利用できません flashLiteを利用すれば動的なサイトを作ることができます。
Adobe Flash Lite
端末毎に識別IDが送信されます
端末からのリクエストの場合、そのHTMLヘッダ、もしくはUserAgentの一部に端末IDが情報として付与されます。IDの仕様はキャリア毎に異なり、契約回線毎に付与されたり、物理機種毎に割り当てられたりとさまざまです。ユーザ認証を保存し、次回から認証なしでアクセスが可能となるモバイルサービスが多いですが、この端末IDを利用していることが多いです。
ケータイの端末ID・ユーザIDの取得についてまとめてみました(ke-tai.org)
対応端末ではGPSデータが利用できます
GPSデータを補足したいCGIへのアクセス時に特別な方法(キャリア毎に異なる)でアクセスするとサーバサイドにアクセス携帯端末の現在位置の緯度経度情報が渡ります。
携帯電話でGPSデータを取得してみよう(ITPro)
絵文字,ショートカットが使えます
携帯ならではの機能として絵文字やショートカット(ダイアル数字ボタンにリンクを割り当てる)が使えます
絵文字変換対応表(docomo)
キャリア判別ライブラリ
アプリケーションを開発する上で、PC/携帯の双方に対応する場合、そのURLを分けているケースがよく見られます.例えば、URLの表記で
http://xxx.co.jp/ (PC版) http://xxx.co.jp/m/ (モバイル版)
という風にアドレスを分けて明記しているサイトもたくさんあります.もちろん実装次第でこのようなURLの区別をユーザにさせることなく動的にPC/モバイルの判別が可能です(PC/モバイル同一URLでサービス提供ができる). UserAgnetからアクセス端末を判別するライブラリに
PHP Pear:Net_UserAgent_Mobile
Perl HTTP-MobileAgent
などがあります。
キャリアが提供する公式ドキュメント
各キャリアから公式に開発ドキュメントが提供されていますのでこちらも参考にすると良いでしょう
作ろうiモードコンテンツ(docomo)
MOBILE CREATION (SoftBank)
EZfactory(au KDDI)
プログラム対応例
例のごとく、演習で利用しているプログラムをもとに、携帯に対応するための改善方法を説明します。一例として参考にしてください。まず修正箇所として、COMMON/RequestManager.class.inc を改造します
<?php require_once('Net/UserAgent/Mobile.php'); class RequestManager { private $_params; function __construct($acceptArgs) { $this->_prams = array(); $agent = &Net_UserAgent_Mobile::singleton(); if($agent->isDoCoMo()||$agent->isVodafone()||$agent->isEZweb()) { $this->add('isMobile', true); } else { $this->add('isMobile', false); } foreach($acceptArgs as $arg) { if(isset($_REQUEST[$arg])) { if($this->get('isMobile')) { $this->add($arg, mb_convert_encoding($_REQUEST[$arg], 'UTF-8', 'SJIS')); } else { $this->add($arg, $_REQUEST[$arg]); } } else { $this->add($arg, ''); } } } 以下同様省略 } ?>
コンストラクタのCGI引数を取得する箇所にデバイス判別を行います. 携帯からのアクセスの場合は isMobileという独自のパラメータをtrueに設定します。また携帯サイトはshift-jisでHTMLが記述されているので、フォーム等からのinputはshift-jisで送信されます。CGI内部ではすべてutf-8として文字列を取り扱いたいのでこの時点で携帯からのアクセスの場合はutf-8への変換を行っています。
次に修正箇所は出力部分です。 VIEW/ViewManager.class.incを修正します
<?php class ViewManager { ... public static function viewSmarty($template, $params = array()) { ... ... if($params['req']['isMobile']) { header("Content-type: text/html; charset=Shift-JIS"); print mb_convert_encoding($smarty->fetch('mobile_'.$template), "SJIS", "UTF-8"); } else { header("Content-type: text/html; charset=utf-8"); $smarty->display($template); } } .... } ?>
isMobileがtrueであった場合は利用するテンプレートはPCのファイルにmobile_が付与されファイルを利用します。(たとえば main.tplであった場合は mobile_main.tpl) Smartyの出力をshift-jisに変換したいので一度メモリに配置する(fetch)で出力後変換し print しています。 テンプレートは各PCで利用するものに mobile_をつけて用意します.文字コードはutf-8でOKです。例えば
{include file='mobile_header.tpl'} モバイルに対応したHTML記述 {include file='mobile_footter.tpl'}
という要領で定義します. headerは文字コードの宣言などPCと異なる部分が多いです。以下のようにします(mobile_header.tpl)
<html xmlns="http://www.w3.org/1999/xhtml" lang="ja" xml:lang="ja"> <head> <meta http-equiv="Content-Type" content="application/xhtml+xml;charset=Shift_JIS"> <meta http-equiv="Pragma" content="no-cache"> <meta http-equiv="Cache-Control" content="no-cache"> <title>ページタイトル</title> </head> <body>
以上の変更でサイトを同じURLで携帯対応ができます。
MySQLを利用した掲示板 プログラム演習9
それではMySQLを利用して簡単な掲示板を作成してみましょう。掲示板の仕様は以下の通りにします
【仕様】
- 掲示板からは名前とコメントを投稿できる
- メイン画面には投稿フォームを設置する
- メイン画面には過去投稿が新しい順に表示できるようにする
- 名前とコメントは両方必須にして、空欄の場合はエラーと画面に表示する
- XSS対策はちゃんとする(ウエブセキュリティ参照)
DBの設計
今回、掲示板情報を保存するDBは前回作成したprog9というDBでTableは以下のbbsというテーブルを一つ作ります
bash-3.2$ cat create_table.sql CREATE TABLE bbs ( id int auto_increment, name varchar(255) not null default '', body text not null default '', pubdate timestamp, primary key(id) ) DEFAULT CHARSET=utf8;
Tableを作成します
bash-3.2$ /Applications/MAMP/Library/bin/mysql -u root -p prog9 < create_table.sql
mysql> show fields from bbs; +---------+--------------+------+-----+-------------------+----------------+ | Field | Type | Null | Key | Default | Extra | +---------+--------------+------+-----+-------------------+----------------+ | id | int(11) | NO | PRI | NULL | auto_increment | | name | varchar(255) | NO | | | | | body | text | NO | | | | | pubdate | timestamp | NO | | CURRENT_TIMESTAMP | | +---------+--------------+------+-----+-------------------+----------------+ 4 rows in set (0.01 sec)
idカラムは自動採番(auto_increment)されるカラムでpubdateは更新(投稿)日付が自動で入るようにしてあります。掲示板に投稿する場合はこのbbsテーブルに name=名前 body=本文で1行インサートします。過去投稿をリストする場合は
SELECT * FROM bbs ORDER BY id DESC;
で取得します。
サイトの開発
基本的には演習6のプログラムを基本として開発します.
|-- ACTION | |-- Action_Base.class.inc | |-- Action_main.class.inc | `-- Action_post.class.inc |-- COMMON | |-- Config.inc | |-- Logs.class.inc | `-- RequestManager.class.inc |-- MODEL | `-- bbsDataAccessMySQL.class.inc |-- VIEW | |-- ViewManager.class.inc | |-- cache | |-- footer.tpl | |-- form.tpl | |-- header.tpl | |-- list.tpl | `-- main.tpl |-- create_table.sql `-- index.php
action=main(default)でメイン画面を表示します。action=postで投稿を行います. Action_post.class.incで投稿を行いますが、投稿完了時に即action=mainにリダイレクトさせます。以下がAction_post.class.incのececute関数です
function execute() { Logs::debug("EXECUTE"); $dataControl = new bbsDataAccessMySQL(); if(($dataControl->saveBBS($this->req->getAll())) == bbsDataAccessMySQL::REQUEST_OK) { header("Location: ./" . $this->templParams['env']['scriptname']); } else { header("Location: ./" . $this->templParams['env']['scriptname'] . "?error=1"); } exit; }
投稿完了後に header("Location")でメイン画面に遷移させます。投稿がエラーの場合はerror=1を付けて遷移させます。MySQLを操作する(投稿をINSERTる。過去投稿をSELECTする)は MODEL/bbsDataAccessMySQL.class.inc で操作します。
bash-3.2$ cat MODEL/bbsDataAccessMySQL.class.inc <?php require_once('./COMMON/HttpApiRequest.class.inc'); class bbsDataAccessMySQL { private $_db; const REQUEST_OK = 0; const REQUEST_ERROR = 1; function __construct() { $this->_db = new mysqli(DB_HOST, DB_USER, DB_PASS, DB_NAME, DB_PORT); $this->_db->query("SET NAMES utf8"); } function __destruct() { $this->_db->close(); } function saveBBS($item) { try { if($item['name'] == "" || $item['body'] == "") { throw new Exception("param error"); } $sql = "INSERT INTO bbs(name, body) values (?, ?)"; if(($stmt = $this->_db->prepare($sql)) === false) { throw new Exception("prepare error"); } if(($stmt->bind_param("ss", $item['name'], $item['body'])) == false) { throw new Exception("binding error"); } if(($stmt->execute()) == false) { throw new Exception("execute error"); } $stmt->close(); return self::REQUEST_OK; } catch (Exception $e){ if($stmt) { $stmt->close; } logs::error($e->getMessage()); return self::REQUEST_ERROR; } } function listBBS() { try { $sql = "SELECT * FROM bbs order by id desc"; if(($result = $this->_db->query($sql)) == false) { throw new Exception("execute error"); } $res = array(); while($row = $result->fetch_array(MYSQLI_ASSOC)) { array_push($res, $row); } return $res; } catch (Exception $e) { logs::error($e->getMessage()); return array(); } } } ?>
最後にSmartyのtemplateですが、今回定義したのはメイン画面のmain.tplだけです。その中身も実際は
bash-3.2$ more VIEW/main.tpl {include file='header.tpl'} {include file='form.tpl'} {include file='list.tpl'} {include file='footer.tpl'}
です。 form.tpl が投稿フォームの部分, list.tplはリスト部分を定義しています. list.tplは
bash-3.2$ cat VIEW/list.tpl <table width="500px;"> {foreach from=$p.bbs item=data} <tr><td> {$data.name|escape:'html'} </td><td> {$data.pubdate} </td></tr> <tr><td colspan="2"> {$data.body|escape:'html'|nl2br} </td></tr> {/foreach} </table>
です。 $data.name $data.bodyの部分は Smartyの変数の修飾子を用いて加工しています。escapeではXSS対策をnl2brは改行をbrタグに置換する処理です。Smartyでhそれ以外にもプラグインという形で変数をtemplate側で加工することが可能です。必要に応じて使うと有効です。
今回のプログラム一式は
http://trac.assembla.com/cX0noKTPir3A7Oab7jnrAJ/browser/prog9
にありますので、必要に応じて参照してください。
【演習】
前回の、地図のAPIと組み合わせて、場所も特定できる掲示板を作成してみましょう。
DBの保存情報に緯度経度を加えます。ポイントは地図を選択した際に form の input hidden の値を書き換えるJavaScriptを書くことが必要になるでしょう. Ajaxで一気に投稿する拡張をすればさらにステップアップできます。