アシアルブログ

アシアルの中の人が技術と想いのたけをつづるブログです

第6回Apache Cordova勉強会 に参加してきました

Cordova/PhoneGap、Monaca向けの勉強会が、2015年6月19日(金)飯田橋IIJ本社 セミナールームにて開催されました。
毎回盛況なユーザ会も今回で6回目となりました。それでは、レポートいってみます。



セッション1: AngularJS入門





AngularJS超入門



まずは、AngularJSの超入門。ちょっと時間が押していたため早足でしたが、一通りの作成方法がプレゼンされました。





AngularJS超入門



発表者: アシアル 岡本さん



超入門というわりには、とくにディレクティブの説明とその利用方法について、バージョンの違いなどが詳しく説明されました。そして最後に、TODOアプリの紹介で締めくくられました。



ざっくりでしたが、AngularJSのcontrollerやFactoryからの、フロントエンドへのバインディング機能は、わずかなコーディング量で実現できるのは素晴らしいと改めて感じさせてくれるプレゼンでした。



セッション2: Cordovaプラグイン開発ライブコーディング



続いて、Cordova女子の登場です。





Animate.cssデモ



壇上は一気に、女子モード全開…プレゼン資料が一面ピンク色でした。



NTTコムウェア 佐藤玲奈さん(右)・古澤麻衣子さん(左)



Crosswalkについて



古澤さんによる、AndroidにおけるWebviewの描画機能の差異を埋めるための、ViewクライアントCrosswalkの紹介です。





NTTコムウェア 古澤さん



発表者:NTTコムウェア 古澤さん



Cordova5.0からプラグイン化され、より簡単に利用出来るようになりました。Android OSバージョンによる表示がうまく統一できないなどで悩むまえに、Crosswalkを利用してみる方が良いかと思います。



楽しいプラグインの基本的な作り方



佐藤さんによる、「プラグイン開発」についての説明です。





NTTコムウェア 佐藤さん



発表者:NTTコムウェア 佐藤さん



プラグインってなに?」という説明から、プラグインを通して、Native機能を実際に利用するところまでを説明されました。丁寧ではありましたが、次回はライブコーディングにチャレンジして貰いたいです!



セッション3: Cordova Plugin をもっと簡単に!



続いて、Cordova Pluginのひな形生成ツールの紹介です。





Sony 関さん



発表者:ソニー 関さん



Plugmanというツールはあるけれど。。。



Plugmanとは、Corodva Pluginのひな形を作ってくれる、コマンドラインツールです。簡単にできるとは言え、問題点も多く、「引数が多く長い」「正しく引数の順序を守らないと動かない」などを抱えているそうです。そこで、YeomanによるCordova Plugin generatorを作ってみたということです。



Plugin開発において、ひな形だけならば、そんなに必要性を感じていませんでしたが、Jasminのテストケースも吐き出してくれると言うことで、興味深い内容でした。



Contribution 歓迎ということですので、興味のある方は是非。



紹介したプラグインのライブコーディング・・・



残念ながら、エラー・・・でした。



資料紹介



GitHub: generator-cordova-plugin-devbed



こちらのページに今回の資料が上がっていますので、ご参考まで。



第6回Cordova勉強会: Cordova plugin をもっと簡単に



その他: ライトニングトークと与太話



最後はライトニングトークでした。実際に開発された方の、生の声が聞けました。



その1「Cordovaで作ったSNSアプリ紹介」



実際に開発をしてみた感想などが中心となりました。こちらの所感は非常に参考になると思います。





池尻野 雄介(インフォコム株式会社)さん



ライトニングトーク: 池尻野 雄介さん(インフォコム株式会社)



ハイブリッドアプリを選んだ理由


  • タイムライン機能のネイティブ化の課題。
  • jQuery Movileだとパフォーマンスチューニングが必須。
  • ClickイベントはFastClickを利用したりしてパフォーマンスを上げた。
  • ハイブリッドアプリ向けにチューニングされたOnsenUIが利用できる。
  • OnsenUI+AngularJSでの実装。


OnsenUIをつかってみての感想


  • ネイティブアプリに遜色ない動き。
  • iOSフラットデザインがベースになっていて、デザインに時間がとられない。
  • AngularJSベースなのでjQueryに比べて保守性が上がる。


Cordovaでハマったところ


  • Android
  • OS4.4.4で画像ファイルのアップロードが出来ない。
  • Cameraプラグインが古い事が原因。
  • Cordovaプラグインのバージョンアップは実施した方が良い。
  • iOS
  • ソフトウェアキーボードの挙動が怪しい。
  • ナビゲーションバーのヘッダが追随せず。結局自前で作成。
  • ionic kyebardプラグインを使うと良いらしい。


所感


  • ハイブリッドで作る決断は正しかった。
  • 業務用なら出来ないことは、ほぼ無い。
  • よほど凝ったUI、ゲームアプリじゃなければ問題無い。


その2「CordovaとElectron(Atom Shell)の同じところ・違うところ」





田中 正裕さん



ライトニングトーク:田中 正裕さん



ELECTRONの紹介です。



ELECTRONとは


もともとは「Atom Shell」と呼ばれていた物ということです。GitHub製のエディタ「ATOM」でも有名です。Node.jsを利用した、ハイブリット実行環境で、GitHubにてオープンソースとして公開されています。他に、ELECTRONの構造やELECTRONとCordovaの違いなどが紹介されました。



アプリケーション開発事例




DEMO


Monacaの基板の元になったコードも紹介されました。



GitHub https://github.com/masahirotanaka/calculator



まとめ



今回は入門向けの発表が多かったですが、それだけCordova、Monacaのユーザ層が広がってきたと言うことでしょう。



初めてのツールなどで開発を行うときは、先人のベストプラクティスを聞いてしまうのが一番の近道です。こういった入門向けの勉強会は、ますます広がりを見せてくれる事でしょう。



これからさらに、活躍の場を広げそうなハイブリッド開発ですが、何事も基礎が大事だとおもった勉強会でした。



勉強会の様子



AngularJS + Onsen UIで始めるPhoneGapアプリケーション

ハイブリッド モバイル アプリ開発フレームワークであるPhoneGapは、
HTML5でアプリを作るための非常に便利なフレームワークです。
カメラや位置情報などネイティブの機能を簡単にJavaScriptから利用できるようになります。

しかしPhoneGapでは、スマートフォンアプリにとってとても重要な、
UIパーツや画面遷移などのインタラクションは提供されていません。
HTML/CSS/JavaScriptでこれらのUI、アニメーションを一から作るのは非常に大変です。
ネイティブと違和感ないデザインや、なめらかな自然な動き、端末毎の差異など気にすることはたくさんあります。

今回ご紹介する、Onsen UI を使用すれば、PhoneGap/Cordovaアプリを飛躍的に改善することができます。
他のフレームワークよりも洗練された柔軟性を持ち、ハイブリッドアプリにおいても、
ネイティブ並みの外観と操作性を、すべてのプラットフォーム上で実現できます。



この記事では、AngularJSとOnsen UIを利用した
PhoneGapアプリケーションの最小構成のアプリを作っていきます。

※今回は、iOS7テーマを利用していますが、Androidテーマやその他のテーマがあります。
 詳しくはOnsen UIのサイトをご覧ください。

■PhoneGapのインストール・プロジェクト作成





$ sudo npm install -g phonegap
$ cordova create hello com.example.hello HelloWorld
$ cd hello/
$ cordova platform add ios
$ cordova run ios


■Onsen UIの設置



HelloWorldプロジェクトのwwwディレクトリの中身は、今回は利用しないので、一旦全て削除してしまいます。

http://onsenui.io/ の「Download」から一式ダウンロードします。

ダウンロードしたonsen_ui.zipを解凍して、
app/lib/onsen ディレクトリを HelloWorldプロジェクトの www/vendors にコピーします。

その他、今回使うファイルを以下のような構成で作成します。


www/
├── css
│   └── style.css
├── index.html
├── js
│   └── app.js
├── page1.html
├── page2.html
└── vendors
    └── onsen


■画面作成



それでは、www/index.htmlから作成していきましょう。
ons-navigatorタグを page1.html を初期ページとして読み込むように設置しています。



<!DOCTYPE html>
<html lang="en" ng-app="myApp">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <link rel="stylesheet" href="vendors/onsen/css/onsenui.css">
        <link rel="stylesheet" href="vendors/onsen/css/topcoat-mobile-onsen-ios7.css">
        <link rel="stylesheet" href="css/style.css" />
        <script src="cordova.js"></script>
        <script src="vendors/onsen/js/angular/angular.js"></script>
        <script src="vendors/onsen/js/angular/angular-touch.js"></script>
        <script src="vendors/onsen/js/onsenui.js"></script>
        <script src="js/app.js"></script>
    </head>
    <body>
        <ons-navigator page="page1.html" title="Page 1">
        </ons-navigator>
    </body>
</html>


page1.htmlでは、ons-list を使ってitemsをリスト表示しています。
www/page1.html


<ons-page class="center">
    <div ng-controller="Page1Ctrl">
        <ons-list>
            <ons-list-item ng-repeat="item in items" ng-click="next($index)">
                {{item}}
            </ons-list-item>
        </ons-list>
    </div>
</ons-page>


それでは、リストを表示するために、プログラム側を記述します。
データを管理する、Dataサービスを作りましょう。

www/js/app.js


(function() {
    var app = angular.module('myApp', ['onsen.directives', 'ngTouch']);

    app.factory('Data', function() {
        var Data = {};
        Data.items = ['aa', 'bb', 'cc'];
        return Data;
    });

    app.controller('Page1Ctrl', function($scope, Data) {
        $scope.items = Data.items;

        $scope.next = function(index) {
            Data.index = index;
            var item = Data.items[index];
            $scope.ons.navigator.pushPage('page2.html', {title: item});
        };
    });

    app.controller('Page2Ctrl', function($scope, Data) {
        $scope.item = Data.items[Data.index];

        $scope.save = function() {
            Data.items[Data.index] = $scope.item;
            $scope.ons.navigator.popPage();
        };
    });
})();


Page1Ctrlでは、Dataサービスのデータを$scopeにセットしています。
また、$scope.next関数を定義して、リストのアイテムがクリックされた場合に、次の画面に遷移する処理を定義しています。

Page2Ctrlでは、ons-text-inputの値を保存する処理を定義しています。
Saveボタンをクリックされると、Dataサービスのデータを更新して、前の画面に戻ります。

page2.htmlも作成してしまいましょう。



<ons-page class="center">
    <div ng-controller="Page2Ctrl">
        <ons-text-input ng-model="item" style="margin:10px;"></ons-text-input><br>
        <ons-button ng-click="save()">Save</ons-button>
    </div>
</ons-page>


■AngularJSのPhoneGap対応


さて、ここまでで、Onsen UI(AngularJS)でのPhoneGapアプリの見た目はできました。
このままでも動作しているように見えますが、
PhoneGapの機能を使う場合には、deviereadyイベントの後に処理を記述する必要があります。

まず、www/index.htmlのng-app属性を削除してAngularJSが自動起動しないようにします。


変更前: <html lang="en" ng-app="myApp">
変更後: <html lang="en">


次に、devicereadyイベントにてAngularJSの起動を行うように変更します。

www/js/app.js


var app = angular.module('myApp', ['onsen.directives', 'ngTouch']);

document.addEventListener('deviceready', function() {
    angular.bootstrap(document, ['myApp']);
}, false);


これで、コントローラ等の処理の中でも、PhoneGapの命令が使えるようになりました。

■完成



実行して動作を確認しましょう。



$ cordova run ios




今回作成した、wwwディレクトリ一式は「www.zip」よりダウンロードできます。

このようにOnsen UIは、MonacaアプリだけでなくPhoneGapアプリでも利用でき、
簡単にHTML5でネイティブのようなUIのスマートフォンアプリを作ることができます。

Onsen UIはフリーのオープンソースソフトウェアです。
登録も必要なく、簡単に使い始めることができますので、
ぜひ試していただき、色々とフィードバックをいただければと思います。

Vision Mobileによるモバイルアプリ開発者の調査レポート

ロンドンに本社を置くモバイル関連のリサーチファームであるVision Mobileが、毎年行なっている世界のアプリ開発者を対象としたアンケート調査のレポートが今年も発表されました。

世界95カ国から3,460人の開発者がアンケートに参加し、加えてインタビュー調査まで行われており、かなりコストがかかっている調査だと思いますが、at&tmozillaNOKIAといったスポンサー企業のおかげで、こちらから無料でレポートを入手することが可能です。



今回は、レポートの第二部で"THE DEVELOPER TOOLS LANDSCAPE"と題して、開発者向けのサードパーティツールに関するレポートが大きなページを割いて展開されていました。この中でアシアルで展開するMonacaがカテゴライズされるCross platform toolsも登場しているので、このあたりを中心に紹介させてもらいます。

まずは、サードパーティツール全般の利用状況ですが、3460人のアンケート回答者の約90%がなんらかのサードパーティツールを利用しているとのこと。個別に見ていくと利用率のトップはAd services(34%)次いでUser analytics(28%)とCross platform tools(27%)が続きます。Cross platform toolsは略称CPTsと呼ぶらしいのですが、私の想定以上にCPTsの利用率が高いですね。一方で、最近よくを話題にBack end as a service(BaaS)は14%とまだまだこれからという感じです。



それでは我々が最も知りたいCPTsの概況に移りたいと思います。CPTsの中で最も使われているのはAdobeのPhoneGap、次いでAppcelerator、Adobe Airと続きます。ゲームに特化したUnityやCoronaも結構使われていますね。

どんな開発者が使っているかというと、HTML開発者が38%とトップ。正確にはクロス集計してみないとわかりませんが、Web技術者がPhoneGapを使ってアプリをつくるというのがCPTsの中心的なスタイルになりつつあるようです。

手前味噌ですが、Monacaが狙っているところがCPTのまさにド真ん中って事ですね。
(その分一番競争の激しいところかも知れません)

グラフには出ていませんが、CPTsを使う開発者は複数(平均1.91)の種類のCPTsを使い分けているとのこと。まだまだ発展途中ということもあり、開発者側も一つのサービスにどっぷりというよりは、色々と保険をかけながら進めている様子がうかがえます。



Monacaの隣接分野ということで、BaaSも見てみましょう。
こちらは、Parseのシェアが28%とダントツで高いようです。2番手に来ているのはIn-house。外部ツールを使わずに自前でBaaSっぽいものを使っているとうことですかね。sencha.ioとACS、それぞれSenchaとAppceleratorが展開するBaaSです。CPTs側がバックエンドまで囲い込んでいくという流れができつつあるのかも知れないですね。このへんの動向はMonacaの事業開発を担当しているものとしては見逃せない流れです。



早くも勝負がつきつつあると思われるカテゴリーがAdServiceとUser analytics。両方ともGoogleが非常に強いですね。User analyticsの分野はGoogleとFlurryの寡占状態になっているようです。Flurryの存在は今回はじめて知りましたが、Cross promotion networksでも上位に入っており、その他にもアプリ開発環境も展開しているようです。





Monacaの事業開発担当者として、非常に有益な発見があったので、Monacaの今後の展開にも活かしていきたいと思います。まずは、このようなグローバルな調査にMonacaの名前が出てくる様にしたいですね。

PhoneGap 2.3.0での変更点

田中です。PhoneGapブログにて、近日リリースされるiOS版PhoneGap 2.3.0の機能ハイライトが紹介されていたので紹介したいと思います。

1. Cordova.plistはconfig.xmlに変更となります

これまでAndroid版ではxmlという形式で、iOS版ではplist形式でPhoneGapに関する設定を記述していました。PhoneGap 2.3.0からは、Androidと同じconfig.xmlのフォーマットに切り替わるようです。

2. ChildBrowserと同じ機能を有する「InAppBrowser」という機能が追加されます

window.openに、下記のような指定方法が可能となります。イベントも受け付けます。



var ref = window.open('http://google.com', '_blank');
ref.addEventListener('loadstart', function(event) { alert(event.type + ' - ' + event.url); } );
ref.addEventListener('loadstop', function(event) { alert(event.type + ' - ' + event.url); } );
ref.addEventListener('exit', function(event) { alert(event.type); } );


なお、targetは_blankだけでなく、_selfと_systemという指定も可能です。_systemとすると、システムのWebブラウザーで開きます。Monacaでいうmonaca.invokeBrowserですね。

3. ホワイトリストの仕様変更

PhoneGapの設定で指定するホワイトリストURLは、今後アプリのメインWebViewのみ適用されるようになります。その結果、プラグインから接続をした場合は対象外となります。

4. デバイスAPIの仕様変更

device.platformが"iPhone"や"iPad"と返していたものが、"iOS"と返されます。一方、device.nameにて"iPhone"などの表記が返されます。

それ以外にも、LocalNotificationイベントをプラグインが受け取る仕組みの追加やCLIコマンドの改良などが加わっています。

最近は少しリリースのペースも落ちてきたかなと思いましたが、まだ仕様変更が含むアップグレードが続きそうです。Monacaでは今のところPhoneGap 2.2をサポートしていますので、現時点でアップデートを行う予定はありませんが、皆さまの参考になれば幸いです。

WebView 中の HTML のデバッグ方法まとめ

iPhone, Android で WebView を用い、HTML5 + CSS3 + Javascript でアプリを開発するケースは、PhoneGap (Cordova) が登場して以来増えつつあるのではないでしょうか。

HTML 開発には Firebug や Web Inspector といったデバッグツールはもはや必須ですよね。
そこで今回は WebView 中の HTML のデバッグ方法をまとめてみました。

1. Android 全般・iOS4 (実機・シミュレータ)・iOS5 (実機)



weinre (ワイナリー)というツールが利用できます。
weinre はウェブサーバとして動作し、そのサーバが提供するスクリプト・ページにアクセスすることで、ブラウザ上でデバッグが行えます。
(なお、この方法は後述する iOS5, 6 においても利用可能です)

・インストール・サーバの起動



weinre は Javascript で記述されており、動作させるには node.js が必要になります。
node.js のインストール方法はいくつかありますが、ここでは nvm を用いてインストールする方法をご紹介します。
nvm (=Node Version Manager) は異なるバージョンの node のインストール・環境切り替えを行うツールで、Ruby における rvm に相当します。


$ git clone git://github.com/creationix/nvm.git ~/nvm
$ . ~/nvm/nvm.sh
$ nvm install 0.8.11
$ nvm alias default 0.8.11


node のインストールが完了したら、weinre をインストールします。


$ npm install weinre


続いて weinre のサーバを起動させます。
デフォルトでは 8080 ポートで待ち受けします。


$ weinre --boundHost -all-


・使用方法



weinre サーバが起動したので、いよいよデバッグを開始しましょう。
まずはデバッグを行いたい HTML の head タグの中に下記を記述します。(IP アドレスは上記サーバを起動させたマシンのものに置き換えて下さい)


<script src="http://192.168.1.99:8080/target/target-script-min.js#identification"></script>

続いて、デバッグツールを動作させる PC で、下記アドレスに WebKit ベースのブラウザでアクセスします。(IP アドレスは上記サーバを起動させたマシンのものに置き換えて下さい)
http://192.168.1.99:8080/client/#identification
その後、アプリを起動し、上記 script タグを埋め込んだページを開くと、先ほどのブラウザにてデバッグが行えるようになります。

また、weinre はマルチユーザに対応しており、上記 URI の #identification の部分を変更すると別のセッションになります。

・所感



weinre は機種を問わず同一の UI で利用できる点が良いですね。
サーバを一個立ちあげておけば複数人で利用できるのも便利です。
難点としては動作速度が少々遅いことがあげられます。

2. iOS5 (シミュレータ)



WebKit の Web Inspector が利用できます。

・使用方法



AppDelegate に下記コードを追加します。


[NSClassFromString(@"WebView") performSelector:@selector(_enableRemoteInspector)];


その後、アプリを起動し、Chromium のバージョン 12 で下記アドレスにアクセスします。
http://localhost:9999/

シミュレータ上からデバッグを行いたいページに移動後、上記ページを更新し、対象の HTML 名のリンクをクリックすればデバッグが行えます。

・所感



iOS5 のシミュレータ専用であり、ブラウザのバージョン(正確にはブラウザが実装している WebSocket のプロトコルのバージョン)も限られる為、汎用性は低いですが、その分高速で Web Inspector ならではの恩恵が受けられます。
利用出来る状況ならば積極的に利用したいですね。

3. iOS6 (実機・シミュレータ)



Safari の Web Inspector が利用できます。

・使用方法



実機・シミュレータにて設定→ Safari →詳細→ Web インスペクタをオンにします。

続いてアプリを起動し、デバッグを行いたいページに移動します。
その後 Safari のメニュー→開発に、シミュレータなら iPhone Simulator、実機なら端末名が表示され、マウスホバーするとページの一覧が表示されますので、対象のページをクリックすれば Web Inspector が起動します。

・所感



XCode 4.5 以上で Safari 6 が必要になりますが、シミュレータ・実機共に高速で Javascript のステップ実行なども行え、とても便利です。
こちらも利用出来る状況ならば積極的に利用したいですね。

以上、駆け足ではありましたが WebView 中の HTML をデバッグする方法まとめでした。

PhoneGap-UserGroup 第1回 勉強会@東京レポート

こんにちは、塚田です。

昨日、PhoneGap-UserGroup 第1回 勉強会@東京に参加してきましたので、その様子をレポートしたいと思います。

Adobe社があるゲートシティ大崎の大ホールに集まったのは約200名のエンジニアの方々。
定員オーバーで参加できなかった人も多数いたとか。

第1回の勉強会からこれだけの人が集まるというのはPhoneGapの注目度が高まっているということですね。

弊社でもこれまで『PhoneGap 入門ガイド』の出版や、公式ドキュメントの邦訳など微力ながらPhoneGapの普及に努めてきましたので、非常にうれしいことです。

まず、主催者である有川さんからPhoneGapの始め方と題して、iOSAndroid向けのPhoneGapの環境構築の説明が有りました。


続いてAdobeエバンジェリストのPlotrさんがPhoneGapの現状と今後の展開について発表。
Parse.comとjQuery Mobileを使ってのライブコーディングで、簡単なロケーションベースのソーシャルネットワークアプリを作る過程を説明してくれました。



その後、PhoneGapの事例発表が続きます。
1番手は株式会社ニーロクの新井さん。ニーロク社はおそらく日本で唯一のHTML5に特化したアプリ開発会社
自社で作られた潮位情報を提供するアプリをPhoneGapの事例として紹介してくれました。



2番手は弊社の田中が務めさせていただきました。
田中からは、Monacaの紹介と弊社で開発している三三株式会社の名刺管理アプリEightの紹介をさせていただきました。
特にアプリの高速化の観点での課題とその解決方法について解説をさせていただきました。



トリを務めたのは、株式会社ICSの池田さん。池田さんはTool Kit for Create JSとPhoneGapを使ってFlashアプリをiOSアプリに変換するデモンストレーションを実施。Flashエンジニアの方も多く参加していたようで、注目度も高かったようです。



内容も盛りだくさんで非常に盛況な勉強会でした。

すでに、9月に第2回の大阪、第3回の東京の開催も決まっているとのことです。
ご興味のある方はPhoneGap-UserGroupのFacebookページで告知がされるとのことなので、そちらをご注目ください。

発表者の皆さん共通に日本語の情報の不足やノウハウの流通不足をPhoneGap普及の課題だと指摘されてましたので、我々も今まで以上にPhoneGap関連情報やHTML5関連のノウハウを積極的に公開したいと思いますのでご期待ください。

Cordova(PhoneGap)のプラグインの作り方

こんにちは、橋本です。

今日はCordova(PhoneGap)のプラグインの作り方について書いていきたいと思います。

Cordovaは、HTML, CSS, Javascriptを組み合わせて、iOSAndroidのネイティブアプリが作れるというものです。
Cordovaで用意されたJavascriptAPIを用いることで、ネイティブの機能を使用することができるようになっています。

「HTML、CSSJavascriptでネイティブアプリが作れるとは!ネイティブの機能も使えるし!これはお手軽!!素晴らしい!!!」

と、思うかもしれませんが、Cordovaの内部では、ネイティブのWebviewの上でHTMLを動かし、JavascriptAPIを通じて予め用意されたネイティブの機能を使っているだけなので、実際にアプリを作り始めると、痒いところに手が届かない場面がしばしばあります。

たとえば、カメラを起動して写真や動画を撮ることはできるのですが、カメラ自体の機能を拡張したり、見た目を変えたりといったことは出来ません。
また、iTunesライブラリへのアクセスや、Bluetoothを使うといったことも、APIが用意されていないので出来ません。

ただ、Cordovaにはプラグインを組み込む機構が用意されているため、Objective-CJavaを使ってネイティブコードでプラグインを作ることで、機能を追加することができるようになっています。


というわけで、今回はiOSの場合を例にプラグインの作り方をご紹介したいと思います。


プラグインを作成するためには、ネイティブのコードと、それをjavascriptから実行するためのjavascriptコードを記述する必要があります。

1. ネイティブコードを作成する
というわけで、まずCordovaプロジェクトのPluginsフォルダの中に、Objective-Cのclassファイルを作成します。
このクラスは、CDVPluginクラスの子クラスとして作成してください。



新規ファイルを作成したら、まずは「.h」ファイルを開いてください。
完成後のソースはこんな感じです。

PGMyPlugin.h


#import <Cordova/CDV.h>

@interface PGMyPlugin : CDVPlugin

@property (nonatomic, copy) NSString *callbackId;

- (void)hello:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
@end


まず、初期状態ではがインポートされているかと思いますが、これをに修正します。

次に、NSString型のcallbackIdというプロパティを定義します。
これは、javascript側から渡されてくるIDを格納しておくためのプロパティで、javascriptに処理の実行結果を返すときに必要になります。
(プロパティ名はcallbackIdでなくても構いません。)

次に、メソッドを定義しています。
今回は「- (void)hello:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;」というメソッドを定義しました。
argumentsとoptionsはどちらも、javascriptから渡される引数なのですが、javascript側から渡した引数の配列の中で、オブジェクトはoptionsに連想配列として渡され、文字列や数値の場合には配列に値が入ってきます。

では、次に「.m」ファイルを開いてメソッドを実装していきたいと思います。
完成後のソースはこんな感じです。

PGMyPlugin.m


#import "PGMyPlugin.h"

@implementation PGMyPlugin
@synthesize callbackId;

- (void)hello:(NSMutableArray *)arguments withDict:(NSMutableDictionary *)options
{
    self.callbackId = [arguments pop];
    
    NSString *name = [arguments objectAtIndex:0];
    CDVPluginResult *result;
    NSString *ret;
    
    if (![name isEqual:@""]) {
        NSString *str = [NSString stringWithFormat:@"Hello %@", [arguments objectAtIndex:0]];
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsString:str];
        ret = [result toSuccessCallbackString:self.callbackId];
    } else {
        result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"名前をください!"];
        ret = [result toErrorCallbackString:self.callbackId];
    }
    
    [self writeJavascript:ret];
}
@end


まず、optionsの一つ目の値としてコールバック用のIDが格納されているので、それをcallbackIdプロパティに格納しています。

次にjavascriptから渡される引数をname変数に格納し、その有無で処理を切り分けています。
CDVPluginResultクラスは、処理結果を作成するためのクラスで、処理結果とステーテスコードを渡して作成します。
また、CDVPluginResultのtoSuccessCallbackStringメソッドや、toErrorCallbackStringメソッドを使うことで、javascriptで定義したコールバック関数を叩くための文字列を作成することができます。
これらのメソッドには、javascriptから渡されたコールバックIDを指定する必要があります。

最後に、writeJavascriptメソッドで、作成した結果の文字列をJavascriptとして実行して完了です。

プラグインの作成が完了したら、このプラグインを実際に使用可能にするために、「Cordova.plist」に登録する必要があります。
具体的には、Cordova.plistのPluginsというDictionaryの中にキーと値を記述します。
キーには、javascript側から呼ぶための名前を記述し、値にはプラグインのクラス名を記述します。
今回は、キーに「jp.co.asial.hello」と記述し、値に「PGMyPlugin」と記述しました。



ネイティブ側はこれで完了です。

2. Javascriptを作成する

次に、プラグインを呼ぶためのjavascriptコードを書いていきます。
完成後のソースはこんな感じです。


<!DOCTYPE HTML>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title></title>
    <script type="text/javascript" charset="utf-8" src="cordova-1.5.0.js"></script>
</head>
<body>
    <input id="input" type="text" />
    <button id="btn">表示</button>
    <script>
        document.addEventListener('deviceready', function(){
            document.querySelector('#btn').addEventListener('click', function(){
                var str = document.querySelector('#input').value;     
                Cordova.exec(function(ret){alert(ret);}, function(error){alert(error);}, 'jp.co.asial.hello', 'hello', [str]);
            });
        });
    </script>
</body>
</html>


javascript側からネイティブのプラグインを呼ぶためには、Cordova.exec()メソッドを使用します。
メソッドの引数は、1つ目から順に、成功時のコールバック関数、失敗時のコールバック関数、Cordova.plistの「キー」に記述した値、呼びたいプラグインのメソッド名、引数の配列となります。

では、実行してみます。



このように、プラグインが実行されていることが確認できるかと思います。

今回は、この簡単なサンプルだけだと物足りないかと思い、iTunesMusicLibraryから音楽を取得するプラグインを作成してみましたので、こっちもよろしければ見てみてください。

PGiTunesMusicLibrary.h


#import <Cordova/CDV.h>
#import <MediaPlayer/MediaPlayer.h>
#import <AVFoundation/AVFoundation.h>

@interface PGiTunesMusicLibrary : CDVPlugin <MPMediaPickerControllerDelegate>

@property (nonatomic, copy) NSString *callbackId;

- (void)showMusicPicker:(NSMutableArray*)arguments withDict:(NSMutableDictionary*)options;
@end


PGiTunesMusicLibrary.m


#import "PGiTunesMusicLibrary.h"


@implementation PGiTunesMusicLibrary
@synthesize callbackId;

- (void)showMusicPicker:(NSMutableArray *)arguments withDict:(NSMutableDictionary *)options
{
    self.callbackId = [arguments pop];
    
    // メディアアイテムピッカーの表示
    MPMediaPickerController *picker = [[MPMediaPickerController alloc] initWithMediaTypes:MPMediaTypeAnyAudio];
    
    
    [picker setDelegate:self];
    [picker setAllowsPickingMultipleItems:YES];
    
    picker.prompt = NSLocalizedString(@"Add songs to play", @"Prompt in media item picker");
    
    [[super viewController] presentModalViewController:picker animated:YES];
}

- (void) mediaPicker: (MPMediaPickerController *) mediaPicker didPickMediaItems: (MPMediaItemCollection *) collection
{
    // @todo: くるくるを表示する
    
    // 音楽ファイルを取り出す
    // AVURLAssetを作成
    MPMediaItem *item = [collection.items lastObject];
    NSURL *url = [item valueForProperty:MPMediaItemPropertyAssetURL];
    AVURLAsset *urlAsset = [AVURLAsset URLAssetWithURL:url
                                               options:nil];
    
    // AVAssetExportSessionを生成
    AVAssetExportSession *exportSession = [[AVAssetExportSession alloc] initWithAsset:urlAsset
                                                                           presetName:AVAssetExportPresetAppleM4A];
    
    exportSession.outputFileType = [[exportSession supportedFileTypes] objectAtIndex:0];
    
    // artworkとmusicファイルを保存するためのURLを作成
    NSString *docDir = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) objectAtIndex:0];
    NSString *baseFilePath = [docDir stringByAppendingPathComponent:[item valueForProperty:MPMediaItemPropertyTitle]];
    NSString *artworkPath = [baseFilePath stringByAppendingPathExtension:@"png"];
    NSString *musicFilePath = [baseFilePath stringByAppendingPathExtension:@"m4a"];
    
    // ArtworkのExport。
    UIImage *artwork = [[item valueForProperty:MPMediaItemPropertyArtwork] imageWithSize:CGSizeMake(100, 100)];
    NSData *artworkData = UIImagePNGRepresentation(artwork);
    
    bool artworkCreated = [artworkData writeToFile:artworkPath
                                        atomically:YES];
    
    // MusicファイルのExport。Exportは非同期に行われるので、完了時にJavascriptに値を返すようにする
    exportSession.outputURL = [NSURL fileURLWithPath:musicFilePath];
    // blocks用にselfを格納
    __block PGiTunesMusicLibrary *blockSelf = self;
    
    [exportSession exportAsynchronouslyWithCompletionHandler:^{
        NSMutableDictionary *dic;
        CDVPluginResult *result;   
        
        if (exportSession.status == AVAssetExportSessionStatusCompleted) {
            dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                   musicFilePath, @"musicUrl",
                   [item valueForProperty:MPMediaItemPropertyTitle], @"title", 
                   nil];
            
            // artworkがある場合のみ
            if (artworkCreated) {
                [dic setObject:artworkPath forKey:@"artworkUrl"];
            }
            
            // javascriptの成功時のコールバックに値を渡す
            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:dic];
            [blockSelf performSelectorOnMainThread:@selector(writeJavascript:)
                                        withObject:[result toSuccessCallbackString:blockSelf.callbackId]
                                     waitUntilDone:YES];
        } else {
            dic = [NSMutableDictionary dictionaryWithObjectsAndKeys:
                   @"Music File Export Error.", @"message",
                   nil];
            
            // javascriptの失敗時のコールバックに値を渡す
            result = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:dic];
            [blockSelf performSelectorOnMainThread:@selector(writeJavascript:)
                                        withObject:[result toErrorCallbackString:blockSelf.callbackId]
                                     waitUntilDone:YES];
        }
        
        [[blockSelf viewController] performSelectorOnMainThread:@selector(dismissModalViewControllerAnimated:)
                                                     withObject:[NSNumber numberWithBool:YES]
                                                  waitUntilDone:YES];
    }];
    
}

- (void) mediaPickerDidCancel:(MPMediaPickerController *)mediaPicker
{
    [[super viewController] dismissModalViewControllerAnimated:YES];
}

@end


index.html


<!DOCTYPE HTML>

<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title></title>
        <script src="http://code.jquery.com/jquery-latest.js"></script>
        <script src="cordova-1.5.0.js" type="text/javascript"></script>
        <style type="text/css">
            #musicList {
                list-style: none;
                padding: 0;
                margin: 5px;
            }

            #musicList li .artwork {
                height: 50px;
                width: 50px;
                border: solid 1px #bbb;
                -webkit-box-shadow: 1px 1px 3px #000;
            }

            #musicList li div.artwork {
                background: -webkit-gradient(linear, left top, left bottom, from(#ddd), to(#fff));
                display: table-cell;
                vertical-align: middle;
                text-align: center;
                font-size: 0.8em;
            }
        </style>
    </head>
    <body>
        <audio id="player" controls></audio>
        <button id="btn">音楽を選択</button>
        <ul id="musicList"></ul>
        <script type="text/javascript">
            document.addEventListener('deviceready', function(){
                var iTunesMusicLibrary = {
                    showMusicPicker: function(types, success, fail) {
                        return Cordova.exec(success, fail, "jp.co.asial.iTunesMusicLibrary", "showMusicPicker", types);
                    }
                };

                var playMusic = function(musicUrl){
                    var $_player = $('#player');
                    $_player
                    .attr({
                        'src': musicUrl    
                    });
                    $_player.get(0).load();
                    $_player.get(0).play();
                };

                var addMusicItem = function(data){
                    var $_list = $('#musicList');

                    // artworkの有無を確認
                    var $_appendedImg;
                    if (data.hasOwnProperty('artworkUrl')) {
                        $_appendedImg = $('<img>')
                        .attr({
                            'src': data.artworkUrl,
                            'class': 'artwork'
                        })
                        .bind('touchend', function(){
                            playMusic(data.musicUrl);      
                        });
                    } else {
                        $_appendedImg = $('<div>')
                        .attr({
                            'class': 'artwork'
                        })
                        .text('No image')
                        .bind('touchend', function(){
                            playMusic(data.musicUrl);      
                        });
                    }

                    $_list.append(
                        $('<li>')
                        .append($_appendedImg)
                        .append(
                            $('<a>')
                            .attr({
                                'href': 'javascript:(function(){return undefined;}())',
                                })
                            .text(data.title)
                            .click(function(){playMusic(data.musicUrl)})
                        )
                    )
                }

                var MusicManager = function(){
                    if (!MusicManager.instance) {
                        var musicList = localStorage.getItem(MusicManager.key);
                        musicList = musicList ? eval('(' + musicList  + ')') : [];

                        this.setMusic = function(data, callback) {
                            data.id = new Date().getTime();
                            musicList.push(data);

                            localStorage.setItem(MusicManager.key, JSON.stringify(musicList));

                            // callbackがある場合は実行
                            if (callback  & & typeof callback === 'function') {
                                callback(data);
                            }
                        };

                        this.getMusic = function(musicId) {
                            if (musicId) {
                                var ret;
                                musicList.forEach(function(item, index, arr){
                                        if (!ret  & & item.id == musicId) {
                                        ret = item;
                                        }     
                                        });

                            } else {
                                ret = musicList;
                            }

                            return ret;
                        }

                        MusicManager.instance = this;
                    }

                    return MusicManager.instance;
                };

                MusicManager.getInstance = function(){
                    if (!this.instance) {
                        this.instance = new MusicManager();
                    }

                    return this.instance;
                }

                MusicManager.key = "MUSIC_LIST";

                $('#btn').click(function(){
                    iTunesMusicLibrary.showMusicPicker([], function(data){
                        // urlにファイルのURL、titleにタイトルが入っているので、localStorageに保存
                        MusicManager.getInstance().setMusic(data, addMusicItem);
                    }, function(error){
                        alert(error.message);
                    });
                });


                // 初期化処理
                MusicManager.getInstance().getMusic().forEach(function(item, index, arr){
                    addMusicItem(item);     
                });
            });
        </script>
    </body>
</html>


ではでは。

PhoneGapアプリ開発のちょっとしたコツ

PhoneGapはスマートフォンにてハイブリッドアプリケーションを作成するためのフレームワークです。この記事では、PhoneGapによりワンソース・マルチユースクロスプラットフォーム)なアプリを開発するためのコツをご紹介します。

従来まで、スマートフォンアプリの開発形態は、

・ネイティブアプリ
・Webアプリ

に大別されていました。ネイティブアプリでは端末の機能を全て活用できる一方で、クロスプラットフォーム性がありません(iOSAndroidで別々に実装)。他方、WebアプリではWebViewを使ったり、ブラウザを使用することで、HTML5などの機能を使用します。これにより、クロスプラットフォーム性が担保されています。一方で、ネイティブ機能を利用できない、などの制約も発生していました。

PhoneGapを使用したハイブリッドアプリは、上記の2つの手法の間に存在し、次の特徴を持っています。

HTML5によるビューの実装
クロスプラットフォーム
・端末機能の使用

一見、良いことだらけのように見えます。しかし、実際にプロジェクトで使用し、iOSAndroidの両方に適用しようとなると、様々な問題につきあたります(当然のことながら)。多くの問題とその解決方法の中から次の4つについてご紹介します。

Android, iOS4, iOS5の区別
javascriptの読み込み
③ DOMContentLoaded
④ ライブラリ(jQuery OR Zepto)



Android, iOS4, iOS5の区別


javascriptによりnavigatorのuserAgent, appVersionなどを使用して切り分けます。特に、iOS4iOS5とではCSSなどで挙動が異なる部分が多く、区別は必須です。以下に一例を示します。



var IS_ANDROID = (/android/gi).test(navigator.appVersion);
var IS_IOS4    = navigator.userAgent.match(/OS 4_[0-9_]+ like Mac OS X/i) !== null;
var IS_IOS5    = navigator.userAgent.match(/OS 5_[0-9_]+ like Mac OS X/i) !== null;


こういった値を何らかのオブジェクトにまとめておくと、とても便利です。


javascriptの読み込み


画面数が増えるに従い、javascriptファイルが増えるに従い、javascriptファイルを読み込み・使用することが多くなります。その一方で、javascript言語の仕様上、依存関係を各ファイルに記述できません。そんな時には、scriptタグを書きだすスクリプトファイルを1つ用意します。そして、そのファイルを各画面にて読み込みます。例えば、次のようになります。

main.js



var IS_ANDROID = (/android/gi).test(navigator.appVersion);

(function() {
  var phonegapJs = '';

  // PhoneGapライブラリの設定
  if(IS_ANDROID) {
    phonegapJs = 'phonegap-android.js';
  } else {
    phonegapJs = 'phonegap-ios.js';
  }

  // 読み込むファイルのリスト生成
  var scripts = [
    // 各種ライブラリ
    'js/vendor/phonegap/' + phonegapJs,
    'js/vendor/zepto/zepto.js',

    // アプリ固有のスクリプト
    'js/app/...',
    '...'
  ];

  for (var i = 0, len = scripts.length; i < len; i++) {
    loadScript(scripts[i]);
  }
})();

function loadScript(filename) {
  // 以下のように書かないと、xcodeでのハイライトがおかしくなる
  // 実際には、普通にdocument.writeでタグを書きだしても良い
  var script = '%3Cscript type="text/javascript" src="' + filename + '"%3E%3C/script%3E';
  document.write(unescape(script));
}



そして、各HTMLファイル内にて、まず上記のスクリプトを読み込みます。

index.html



<html>
<head>
  <script type="text/javascript" src="js/main.js"></script>
  ...
</head>
<body>
  ...
</body>
</html>


極々当たり前のことですが、これだけでかなり楽になります。同時に、javascriptファイルを分割できるため、コード自体も読みやすくなります(分割には賛否両論ありそうですが・・・)。また、require.jsなど、似たような機能を持ったライブラリもあります。興味のある方は、お試しください。

③ DOMContentLoaded


DOMContentLoadedイベント内にて、devicereadyイベントをバインドしましょう。DOMContentLoadedイベントは、DOMの読み込みが終わった際に発生します(これ以降、DOMを使えます)。

PhoneGapでは、プラグイン機構を通じてネイティブ機能へアクセス可能です。ただし、アクセスできる条件として、PhoneGapにより、javascriptのdevicereadyイベントが発生している必要があります。



document.addEventListener('deviceready', function(){
  // この中でアプリの内容・ロジックを実行する
});


実際にアプリの内容を実行する際には、DOMを活用したり、CSSを使ったりと、HTMLの構造を必ず利用します。そのため、HTML構造などが読み込まれてからdevicereadyをバインドする必要があります。次のように、BODYタグのonLoadやloadイベントでバインドすることも可能です。



<html>
<head>
  <script type="text/javascript" src="phonegap.js"></script>
  <script type="text/javascript">
    function onBodyLoad() {
      document.addEventListener('deviceready', onDeviceReady);
    }

    function onDeviceReady() {
      // PhoneGap開始後の処理を記述する
    }
</script>
</head>
<body onLoad="onBodyLoad();">
  <div>...</div>
  <div>...</div>
  <div>...</div>
</body>
</html>


ただ、onLoadやloadイベントはDOMContentLoadedイベントに比べて遅くなる場合があります。というのも、画像ファイル等の読み込みも待ってしまうからです。そのため、アプリの開始が遅れる恐れがあります。DOMContentLoaded発生時にdevicereadyをバインドする方法は、次の通りです。



<html>
<head>
  <script type="text/javascript" src="phonegap.js"></script>
  <script type="text/javascript">
    document.addEventListener('DOMContentLoaded', function(){
        document.addEventListener('deviceready', onDeviceReady);
    });

    function onDeviceReady() {
      // PhoneGap開始後の処理を記述する
    }
  </script>
</head>
<body>
  <div>...</div>
  <div>...</div>
  <div>...</div>
</body>
</html>


④ ライブラリ(jQuery OR Zepto)


javascriptの操作性や利便性を向上させるライブラリとしては、Zeptoを使いましょう。ZeptoはjQeuryからIE対応を取り除いた、軽いライブラリです(jQueryに比べて)。スマートフォン用のjQueryと思って下さい。jQueryも便利ですが、スマートフォンで使うとかなり重くなります。



まずは、極々基本的なお話をしました。実際、使ってみると、PhoneGapやHTML5でのスマートフォンアプリ開発がかなりスムーズになります。機会があれば、ぜひ試してみて下さい。

【PhoneGap】「PhoneGap入門ガイド」が発売されました!

こんにちは。

仕事のことを省みず、OSX Lionが出た瞬間に仕事用のMacbookAirをMacOSX Lionにアップデートした橋本です。
今のところ、特に不具合は無さそうなので、ホッと胸をなでおろしております。

それはさておき、本題へ。


去る7/22に、翔泳社から「PhoneGap入門ガイド」というタイトルで、PhoneGapの入門本が発売されました。




自分も共著者として執筆に参加させていただきましたので、書店で見かけたときには是非手にとってみてもらえると嬉しいです。


この本の内容ですが、初めてPhoneGapを触る人を対象に、PhoneGapで開発を行うための環境構築、簡単なアプリケーションの作成、作成したアプリケーションのAppStoreおよびAndroidマーケットへの登録までの流れをひと通り網羅した、大変盛りだくさんの内容となっております。

自画自賛ではありますが、これ一冊あれば、PhoneGapを触ったことの無い人でも、PhoneGapで作ったiPhone/Androidアプリをリリースするところまでたどり着くことが出来る、なかなかの良書なのではないかと。

PhoneGapを使えば、HTML、CSSJavascriptを用いて、iOSAndroid向けのネィティブアプリケーションを作成することが出来るので、

スマホ向けのアプリ作ってみたいけど、Objective-CJavaも書けないし…」

と、なんだかんだでスマホアプリの作成に躊躇していたWebエンジニアの皆様。
是非「PhoneGap入門ガイド」を参考にしつつ、スマホ向けアプリの作成に挑戦してみください。

アプリの作成で行き詰まったときには、弊社で運営させていただいているPhoneGap Fanに日本語のAPIリファレンスなどもありますので、そちらも参考にしていただければと思います。

【PhoneGap】Xcode4でPhoneGapプロジェクトを作成する方法

こんにちは。
最近iPad2を買い、コードを書くとき以外は一切家でPCを触らなくなった橋本です。
ネットサーフィン、メール、動画、ゲームくらいの用途であればiPad2で全て賄えますね。
iPad2最高!

iPad2賛辞はさておき、今日はそんなiPadのネィティブアプリを簡単に作れるPhoneGapについて書いていきたいと思います。

PhoneGapとは何かと言いますと、HTML & CSS & JavaScriptiPhone(iPad)/Androidのネイティブアプリが作れるフレームワークです。

詳しくは、以前の弊社鴨田のブログ【デザイナー必見】iPhone/AndroidアプリをHTML+Javascriptで作成(PhoneGapのススメ)や、PhoneGapの公式ページPhoneGap.com、弊社で運営しているPhoneGap Fanを参考にしてください。

PhoneGap Fanや、PhoneGap.comを見てもらうと分かるかと思いますが、Eclipseを使ったAndroid版のプロジェクト作成方法とXcode3を使ったiPhone(iPad)プロジェクトの作成方法はどちらにも載っています。

ただ、どちらにも「Xcode4を使ったiPhone(iPad)プロジェクト作成方法」が含まれていません。

今回は、どちらにも載っていない「Xcode4を使ったiPhone(iPad)プロジェクト作成方法」について書いていきます。


Eclipseを使ってAndroid版を作る場合、もしくはXcode3を使ってプロジェクトを作成する場合は、PhoneGap Fanのインストール方法を見てもらえば分かるとおり、非常に簡単にプロジェクトを作成することが出来るのですが、Xcode4を使う場合、PhoneGapが正式にXcode4に対応していないため、公式ページから配付されているインストーラを使ってプロジェクトを作成することが出来ません。

幸い、PhoneGap公式のGitHubで、Xcode4を使ってプロジェクトを作成するためのインストーラが配付されているので、今回はそちらを使用します。
インストーラの配付元

配付元のReadmeにも書いてあるように、まずインストーラをMakeする必要があるため、Macのターミナルを開いて以下のコマンドを叩きます。



cd ~/Downloads/phonegap-phonegap-iphone-xxxxxx/
make


「~/Downloads/phonegap-phonegap-iphone-xxxxxx/」は、DLしたファイルの保存先です。
上記のコマンドをターミナルで叩くと、同じフォルダ内に「PhoneGapInstaller.pkg」という名前のインストーラが出来ていると思いますので、それを使ってPhoneGapをインストールします。

インストールが完了したら、Xcode4を起動して新規プロジェクトの作成を選択し、テンプレートの選択画面でiOS > Application > PhoneGap-based-Applicationを選択して、適当なプロジェクト名を付けてプロジェクトを作成します。



これで、プロジェクトの作成は完了なのですが、このままで実行するとエラーが出ます。
えぇ、まだ終わりではないのです。

エラーを消すためには、以下の手順を実行してください。

1.wwwフォルダをプロジェクトに追加する。


Xcodeの「File > Add Files to プロジェクト名」でファイルを追加します。プロジェクトの直下にwwwというフォルダがあるので、そのフォルダを選択し、Destinationという項目の「Copy items into destination group's folder(if needed)」のチェックを外し、Foldersという項目の「Create folder references for any added folders」を選択して、Addボタンを押します。



2.Build PhasesのCopy Bundle Resourcesから、いらない項目を削除する。


サイドバーのプロジェクト名を選択し、TARGETSのプロジェクト名を選択する。
タブの中から、Build Phasesを選択し、Copy Bundle Resourcesから、以下のファイルを削除する。
・プロジェクト名-info.plist
・AppDelegate.h
・README


これでエラーが消えます。
詳しくは、公式の動画を見てください。
PhoneGap Installer - Xcode 4 Template [HD]

これでエラーは表示されなくなったので、あとは、wwwフォルダの中のindex.htmlを修正してアプリを作るだけですね。

簡単なサンプルアプリは、PhoneGap Fanに載っているので、上記の方法でプロジェクトを作って、ぜひ動かしてみてください!

サンプルアプリ配付元